This commit is contained in:
@@ -0,0 +1,222 @@
|
|||||||
|
package org.lib3270j.telnet;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.lib3270j.ConnectionConfig;
|
||||||
|
import org.lib3270j.ConnectionState;
|
||||||
|
import org.lib3270j.TerminalModel;
|
||||||
|
import org.lib3270j.charset.EbcdicTranslator;
|
||||||
|
import org.lib3270j.datastream.DataStreamProcessor;
|
||||||
|
import org.lib3270j.protocol.TelnetConstants;
|
||||||
|
import org.lib3270j.screen.ScreenBuffer;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
public class TelnetFSMTest {
|
||||||
|
|
||||||
|
private ConnectionConfig config;
|
||||||
|
private ScreenBuffer screenBuffer;
|
||||||
|
private DataStreamProcessor dsProcessor;
|
||||||
|
private TelnetFSM fsm;
|
||||||
|
private MockConnection connection;
|
||||||
|
|
||||||
|
private static class MockConnection extends TelnetConnection {
|
||||||
|
final List<byte[]> sentData = new ArrayList<>();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setup() {
|
||||||
|
config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
|
||||||
|
screenBuffer = new ScreenBuffer(TerminalModel.IBM_3279_4, new EbcdicTranslator());
|
||||||
|
dsProcessor = new DataStreamProcessor(screenBuffer, new EbcdicTranslator());
|
||||||
|
fsm = new TelnetFSM(config, screenBuffer, dsProcessor);
|
||||||
|
connection = new MockConnection(config, fsm);
|
||||||
|
fsm.setConnection(connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void feedBytes(int... bytes) {
|
||||||
|
for (int b : bytes) {
|
||||||
|
fsm.feedByte(b & 0xFF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testHostPrefixParsingPlainTn3270() {
|
||||||
|
ConnectionConfig c1 = ConnectionConfig.parseHostString("P:mainframe.example.com", 23, TerminalModel.IBM_3279_4);
|
||||||
|
assertFalse(c1.isTn3270eEnabled(), "P: prefix should disable TN3270E");
|
||||||
|
assertFalse(c1.isUseTls());
|
||||||
|
assertEquals("mainframe.example.com", c1.getHost());
|
||||||
|
|
||||||
|
ConnectionConfig c2 = ConnectionConfig.parseHostString("plain:zos.net:2323", 23, TerminalModel.IBM_3279_4);
|
||||||
|
assertFalse(c2.isTn3270eEnabled());
|
||||||
|
assertEquals("zos.net", c2.getHost());
|
||||||
|
assertEquals(2323, c2.getPort());
|
||||||
|
|
||||||
|
ConnectionConfig c3 = ConnectionConfig.parseHostString("L:P:secure.mainframe.com:992", 0, TerminalModel.IBM_3279_4);
|
||||||
|
assertTrue(c3.isUseTls(), "L: should enable TLS");
|
||||||
|
assertFalse(c3.isTn3270eEnabled(), "P: should disable TN3270E");
|
||||||
|
assertEquals("secure.mainframe.com", c3.getHost());
|
||||||
|
assertEquals(992, c3.getPort());
|
||||||
|
|
||||||
|
ConnectionConfig c4 = ConnectionConfig.parseHostString("non-e:vm.ibm.com", 23, TerminalModel.IBM_3279_4);
|
||||||
|
assertFalse(c4.isTn3270eEnabled());
|
||||||
|
assertEquals("vm.ibm.com", c4.getHost());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPlainTn3270NegotiationRejectsTn3270E() {
|
||||||
|
config.setTn3270eEnabled(false);
|
||||||
|
fsm.onConnected();
|
||||||
|
assertEquals(ConnectionState.TELNET_PENDING, fsm.getConnectionState());
|
||||||
|
|
||||||
|
// Host offers DO TN3270E -> client must reply WONT TN3270E
|
||||||
|
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TN3270E);
|
||||||
|
assertFalse(fsm.getMyOpts()[TelnetConstants.TELOPT_TN3270E]);
|
||||||
|
|
||||||
|
// Host offers WILL TN3270E -> client must reply DONT TN3270E
|
||||||
|
feedBytes(TelnetConstants.IAC, TelnetConstants.WILL, TelnetConstants.TELOPT_TN3270E);
|
||||||
|
assertFalse(fsm.getHisOpts()[TelnetConstants.TELOPT_TN3270E]);
|
||||||
|
|
||||||
|
// Host negotiates standard 3270 options: DO BINARY, WILL BINARY, DO EOR, WILL EOR, DO TTYPE
|
||||||
|
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);
|
||||||
|
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TTYPE);
|
||||||
|
|
||||||
|
// Client should have transitioned to CONNECTED_3270
|
||||||
|
assertEquals(ConnectionState.CONNECTED_3270, fsm.getConnectionState());
|
||||||
|
|
||||||
|
// Host asks for TTYPE
|
||||||
|
feedBytes(TelnetConstants.IAC, TelnetConstants.SB, TelnetConstants.TELOPT_TTYPE,
|
||||||
|
TelnetConstants.TELQUAL_SEND, TelnetConstants.IAC, TelnetConstants.SE);
|
||||||
|
|
||||||
|
// Verify sent packets contain WONT TN3270E, DONT TN3270E, and TTYPE IS
|
||||||
|
boolean foundWontTn3270e = false;
|
||||||
|
boolean foundTtypeIs = false;
|
||||||
|
for (byte[] pkt : connection.sentData) {
|
||||||
|
if (pkt.length == 3 && (pkt[0] & 0xFF) == TelnetConstants.IAC &&
|
||||||
|
(pkt[1] & 0xFF) == TelnetConstants.WONT && (pkt[2] & 0xFF) == TelnetConstants.TELOPT_TN3270E) {
|
||||||
|
foundWontTn3270e = true;
|
||||||
|
}
|
||||||
|
if (pkt.length >= 4 && (pkt[0] & 0xFF) == TelnetConstants.IAC &&
|
||||||
|
(pkt[1] & 0xFF) == TelnetConstants.SB && (pkt[2] & 0xFF) == TelnetConstants.TELOPT_TTYPE) {
|
||||||
|
foundTtypeIs = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue(foundWontTn3270e, "Expected WONT TN3270E response");
|
||||||
|
assertTrue(foundTtypeIs, "Expected TTYPE IS response");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTn3270eFunctionsNegotiationWithoutBindImageTransitionsToConnected() {
|
||||||
|
config.setTn3270eEnabled(true);
|
||||||
|
fsm.onConnected();
|
||||||
|
assertEquals(ConnectionState.TELNET_PENDING, fsm.getConnectionState());
|
||||||
|
|
||||||
|
// Host offers DO TN3270E -> client accepts with WILL TN3270E
|
||||||
|
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TN3270E);
|
||||||
|
assertTrue(fsm.getMyOpts()[TelnetConstants.TELOPT_TN3270E]);
|
||||||
|
|
||||||
|
// Host responds with DEVICE-TYPE IS IBM-3279-4-E CONNECT LU01
|
||||||
|
byte[] devTypeIs = new byte[]{
|
||||||
|
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SB, (byte) TelnetConstants.TELOPT_TN3270E,
|
||||||
|
0x02, 0x04, // OP_DEVICE_TYPE, OP_IS
|
||||||
|
'I', 'B', 'M', '-', '3', '2', '7', '9', '-', '4', '-', 'E',
|
||||||
|
0x01, // OP_CONNECT
|
||||||
|
'L', 'U', '0', '1',
|
||||||
|
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SE
|
||||||
|
};
|
||||||
|
for (byte b : devTypeIs) fsm.feedByte(b & 0xFF);
|
||||||
|
|
||||||
|
// Host responds with FUNCTIONS REQUEST <empty> (e.g. pytn3270 / RFC 2355 server)
|
||||||
|
byte[] funcsReq = new byte[]{
|
||||||
|
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SB, (byte) TelnetConstants.TELOPT_TN3270E,
|
||||||
|
0x03, 0x07, // OP_FUNCTIONS, OP_REQUEST
|
||||||
|
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SE
|
||||||
|
};
|
||||||
|
for (byte b : funcsReq) fsm.feedByte(b & 0xFF);
|
||||||
|
|
||||||
|
// Per RFC 2355: Client must reply with FUNCTIONS IS, and without BIND-IMAGE, session is bound immediately
|
||||||
|
assertEquals(ConnectionState.CONNECTED_TN3270E, fsm.getConnectionState(),
|
||||||
|
"Expected CONNECTED_TN3270E when BIND-IMAGE is not negotiated");
|
||||||
|
|
||||||
|
// Verify that client sent SB TN3270E FUNCTIONS IS SE
|
||||||
|
boolean foundFunctionsIs = false;
|
||||||
|
for (byte[] pkt : connection.sentData) {
|
||||||
|
if (pkt.length >= 5 && (pkt[0] & 0xFF) == TelnetConstants.IAC &&
|
||||||
|
(pkt[1] & 0xFF) == TelnetConstants.SB && (pkt[2] & 0xFF) == TelnetConstants.TELOPT_TN3270E &&
|
||||||
|
(pkt[3] & 0xFF) == 0x03 && (pkt[4] & 0xFF) == 0x04) {
|
||||||
|
foundFunctionsIs = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue(foundFunctionsIs, "Expected FUNCTIONS IS reply from client upon receiving FUNCTIONS REQUEST");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPlainDataStreamFallbackInTn3270eMode() {
|
||||||
|
config.setTn3270eEnabled(true);
|
||||||
|
fsm.onConnected();
|
||||||
|
|
||||||
|
// Negotiate TN3270E with empty functions
|
||||||
|
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TN3270E);
|
||||||
|
byte[] devTypeIs = new byte[]{
|
||||||
|
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SB, (byte) TelnetConstants.TELOPT_TN3270E,
|
||||||
|
0x02, 0x04, 'I', 'B', 'M', '-', '3', '2', '7', '9', '-', '4', '-', 'E',
|
||||||
|
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SE
|
||||||
|
};
|
||||||
|
for (byte b : devTypeIs) fsm.feedByte(b & 0xFF);
|
||||||
|
|
||||||
|
byte[] funcsReq = new byte[]{
|
||||||
|
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SB, (byte) TelnetConstants.TELOPT_TN3270E,
|
||||||
|
0x03, 0x07, (byte) TelnetConstants.IAC, (byte) TelnetConstants.SE
|
||||||
|
};
|
||||||
|
for (byte b : funcsReq) fsm.feedByte(b & 0xFF);
|
||||||
|
assertEquals(ConnectionState.CONNECTED_TN3270E, fsm.getConnectionState());
|
||||||
|
|
||||||
|
// Now host sends raw 3270 data stream without 5-byte TN3270E header (e.g. EraseWrite 0xF5)
|
||||||
|
// 0xF5 = EraseWrite, 0xC3 = WCC, 0x11 = SBA, 0x40 0x40 = Pos 0, "HELLO"
|
||||||
|
EbcdicTranslator trans = new EbcdicTranslator();
|
||||||
|
String msg = "HELLO";
|
||||||
|
byte[] rawStream = new byte[3 + 2 + msg.length() + 2];
|
||||||
|
rawStream[0] = (byte) 0xF5; // EraseWrite
|
||||||
|
rawStream[1] = (byte) 0xC3; // WCC
|
||||||
|
rawStream[2] = 0x11; // SBA
|
||||||
|
rawStream[3] = 0x40; rawStream[4] = 0x40; // addr 0
|
||||||
|
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);
|
||||||
|
|
||||||
|
// State must have automatically switched to CONNECTED_3270 and screen updated
|
||||||
|
assertEquals(ConnectionState.CONNECTED_3270, fsm.getConnectionState());
|
||||||
|
assertEquals('H', trans.ebcdicToUnicode(screenBuffer.getCellEC(0)));
|
||||||
|
assertEquals('E', trans.ebcdicToUnicode(screenBuffer.getCellEC(1)));
|
||||||
|
assertEquals('L', trans.ebcdicToUnicode(screenBuffer.getCellEC(2)));
|
||||||
|
assertEquals('L', trans.ebcdicToUnicode(screenBuffer.getCellEC(3)));
|
||||||
|
assertEquals('O', trans.ebcdicToUnicode(screenBuffer.getCellEC(4)));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user