Add overflow protections based on testing moshix' bbs
This commit is contained in:
@@ -69,6 +69,10 @@ public class ConnectionConfig {
|
||||
private int reconnectMaxRetries = 5;
|
||||
private int tcpUserTimeoutMs = 0;
|
||||
|
||||
// Framing buffer sanity limits (RFC 885 / RFC 2355 EOR protection)
|
||||
public static final int DEFAULT_MAX_FRAMING_BUFFER_SIZE = 1048576; // 1MB (supports dense GDDM graphics, symbol sets, and DFT transfers)
|
||||
private int maxFramingBufferSize = DEFAULT_MAX_FRAMING_BUFFER_SIZE;
|
||||
|
||||
// Host operating system type (TSO, CMS, CICS)
|
||||
private String hostType = "TSO";
|
||||
private boolean savedConnection = false;
|
||||
@@ -549,6 +553,11 @@ public class ConnectionConfig {
|
||||
public String getAssociatedPrinterLu() { return associatedPrinterLu; }
|
||||
public void setAssociatedPrinterLu(String printerLu) { this.associatedPrinterLu = printerLu; }
|
||||
|
||||
public int getMaxFramingBufferSize() { return maxFramingBufferSize; }
|
||||
public void setMaxFramingBufferSize(int maxFramingBufferSize) {
|
||||
this.maxFramingBufferSize = maxFramingBufferSize > 0 ? maxFramingBufferSize : DEFAULT_MAX_FRAMING_BUFFER_SIZE;
|
||||
}
|
||||
|
||||
public haus.nightmare.lib3270j.printer.PrinterConfig toPrinterConfig() {
|
||||
haus.nightmare.lib3270j.printer.PrinterConfig pcfg = new haus.nightmare.lib3270j.printer.PrinterConfig(host, port);
|
||||
pcfg.setUseTls(useTls);
|
||||
@@ -559,6 +568,7 @@ public class ConnectionConfig {
|
||||
pcfg.setPrinterLuName(associatedPrinterLu);
|
||||
pcfg.setAssociatedDisplayLuName(luName);
|
||||
pcfg.setCodePage(codePage);
|
||||
pcfg.setMaxFramingBufferSize(maxFramingBufferSize);
|
||||
return pcfg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,21 +17,49 @@ import java.util.Arrays;
|
||||
*/
|
||||
public class FastByteBuffer extends OutputStream {
|
||||
|
||||
public static final int DEFAULT_INITIAL_CAPACITY = 32768;
|
||||
public static final int DEFAULT_MAX_CAPACITY = 1048576; // 1MB (matches modern x3270 PDS_MAX for dense GDDM / GOCA vector graphics)
|
||||
|
||||
private final int initialCapacity;
|
||||
private int maxCapacity;
|
||||
private byte[] buf;
|
||||
private int count;
|
||||
|
||||
public FastByteBuffer() {
|
||||
this(32768);
|
||||
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_MAX_CAPACITY);
|
||||
}
|
||||
|
||||
public FastByteBuffer(int initialCapacity) {
|
||||
this.buf = new byte[Math.max(32, initialCapacity)];
|
||||
this(initialCapacity, Math.max(initialCapacity, DEFAULT_MAX_CAPACITY));
|
||||
}
|
||||
|
||||
public FastByteBuffer(int initialCapacity, int maxCapacity) {
|
||||
this.initialCapacity = Math.max(32, initialCapacity);
|
||||
this.maxCapacity = Math.max(this.initialCapacity, maxCapacity);
|
||||
this.buf = new byte[this.initialCapacity];
|
||||
this.count = 0;
|
||||
}
|
||||
|
||||
public int getMaxCapacity() {
|
||||
return maxCapacity;
|
||||
}
|
||||
|
||||
public void setMaxCapacity(int maxCapacity) {
|
||||
this.maxCapacity = Math.max(this.initialCapacity, maxCapacity);
|
||||
}
|
||||
|
||||
public int getInitialCapacity() {
|
||||
return initialCapacity;
|
||||
}
|
||||
|
||||
private void ensureCapacity(int minCapacity) {
|
||||
if (minCapacity > maxCapacity) {
|
||||
throw new FramingBufferOverflowException(
|
||||
"FastByteBuffer exceeded maximum capacity of " + maxCapacity + " bytes (requested: " + minCapacity + ")"
|
||||
);
|
||||
}
|
||||
if (minCapacity > buf.length) {
|
||||
int newCap = Math.max(buf.length << 1, minCapacity);
|
||||
int newCap = Math.min(maxCapacity, Math.max(buf.length << 1, minCapacity));
|
||||
buf = Arrays.copyOf(buf, newCap);
|
||||
}
|
||||
}
|
||||
@@ -64,6 +92,9 @@ public class FastByteBuffer extends OutputStream {
|
||||
|
||||
public void reset() {
|
||||
count = 0;
|
||||
if (buf.length > maxCapacity) {
|
||||
buf = new byte[maxCapacity];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package haus.nightmare.lib3270j.datastream;
|
||||
|
||||
/**
|
||||
* Thrown when an inbound framing buffer exceeds its maximum configured capacity
|
||||
* (typically 64KB) without encountering an end-of-record delimiter (IAC EOR).
|
||||
*/
|
||||
public class FramingBufferOverflowException extends RuntimeException {
|
||||
|
||||
public FramingBufferOverflowException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public FramingBufferOverflowException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,7 @@ public class FTDft {
|
||||
private int pendingByte = -1;
|
||||
|
||||
// Record formatting buffers and SO/SI tracking
|
||||
private static final int MAX_DFT_RECORD_BUFFER_SIZE = 131072; // 128KB sanity cap
|
||||
private final ByteArrayOutputStream downloadRecordBuffer = new ByteArrayOutputStream();
|
||||
private boolean dbcsMode = false;
|
||||
private boolean hasPendingByteInDBCS = false;
|
||||
@@ -549,9 +550,17 @@ public class FTDft {
|
||||
int lrecl = (config != null && config.getLrecl() > 0) ? config.getLrecl() : hostLrecl;
|
||||
|
||||
if (isRecfmV) {
|
||||
if (downloadRecordBuffer.size() + length > MAX_DFT_RECORD_BUFFER_SIZE) {
|
||||
downloadRecordBuffer.reset();
|
||||
throw new IOException("FTDft download record buffer exceeded sanity limit of " + MAX_DFT_RECORD_BUFFER_SIZE + " bytes");
|
||||
}
|
||||
downloadRecordBuffer.write(data, offset, length);
|
||||
processVariableDownloadRecords(config);
|
||||
} else if (isRecfmF && lrecl > 0) {
|
||||
if (downloadRecordBuffer.size() + length > MAX_DFT_RECORD_BUFFER_SIZE) {
|
||||
downloadRecordBuffer.reset();
|
||||
throw new IOException("FTDft download record buffer exceeded sanity limit of " + MAX_DFT_RECORD_BUFFER_SIZE + " bytes");
|
||||
}
|
||||
downloadRecordBuffer.write(data, offset, length);
|
||||
processFixedDownloadRecords(lrecl, config);
|
||||
} else {
|
||||
|
||||
@@ -38,6 +38,7 @@ public class NvtProcessor {
|
||||
private static final int STATE_OSC = 4;
|
||||
private static final int STATE_OSC_ESC = 5;
|
||||
|
||||
private static final int MAX_ESC_BUFFER_SIZE = 4096;
|
||||
private int parseState = STATE_NORMAL;
|
||||
private final ByteArrayOutputStream escBuffer = new ByteArrayOutputStream();
|
||||
|
||||
@@ -470,32 +471,51 @@ public class NvtProcessor {
|
||||
} else if (parseState == STATE_OSC) {
|
||||
if (b == 0x07) { // BEL terminator
|
||||
handleOSC(escBuffer.toByteArray());
|
||||
escBuffer.reset();
|
||||
parseState = STATE_NORMAL;
|
||||
} else if (b == 0x1B) { // Possible ESC \ (ST)
|
||||
parseState = STATE_OSC_ESC;
|
||||
} else if (b == 0x9C) { // 8-bit ST
|
||||
handleOSC(escBuffer.toByteArray());
|
||||
escBuffer.reset();
|
||||
parseState = STATE_NORMAL;
|
||||
} else {
|
||||
escBuffer.write(b);
|
||||
if (escBuffer.size() < MAX_ESC_BUFFER_SIZE) {
|
||||
escBuffer.write(b);
|
||||
} else {
|
||||
escBuffer.reset();
|
||||
parseState = STATE_NORMAL;
|
||||
}
|
||||
}
|
||||
} else if (parseState == STATE_OSC_ESC) {
|
||||
if (b == '\\') { // ESC \ (ST) terminator
|
||||
handleOSC(escBuffer.toByteArray());
|
||||
escBuffer.reset();
|
||||
parseState = STATE_NORMAL;
|
||||
} else {
|
||||
// False alarm on ST, put ESC and byte into OSC buffer
|
||||
escBuffer.write(0x1B);
|
||||
escBuffer.write(b);
|
||||
parseState = STATE_OSC;
|
||||
if (escBuffer.size() + 2 <= MAX_ESC_BUFFER_SIZE) {
|
||||
escBuffer.write(0x1B);
|
||||
escBuffer.write(b);
|
||||
parseState = STATE_OSC;
|
||||
} else {
|
||||
escBuffer.reset();
|
||||
parseState = STATE_NORMAL;
|
||||
}
|
||||
}
|
||||
} else if (parseState == STATE_CSI) {
|
||||
escBuffer.write(b);
|
||||
if (escBuffer.size() < MAX_ESC_BUFFER_SIZE) {
|
||||
escBuffer.write(b);
|
||||
}
|
||||
// CSI final bytes are in the range 0x40..0x7E
|
||||
if (b >= 0x40 && b <= 0x7E) {
|
||||
byte[] seq = escBuffer.toByteArray();
|
||||
escBuffer.reset();
|
||||
curAddr = processAnsiEscapeSequence(seq, curAddr, rows, cols);
|
||||
parseState = STATE_NORMAL;
|
||||
} else if (escBuffer.size() >= MAX_ESC_BUFFER_SIZE) {
|
||||
escBuffer.reset();
|
||||
parseState = STATE_NORMAL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,8 @@ public class PrinterConfig {
|
||||
private long autoFlushTimeoutMs = 2000;
|
||||
private int graphicErrorAction = PrinterConstants.GEA_NO_SUBSTITUTION;
|
||||
private char graphicErrorReplacementChar = '?';
|
||||
public static final int DEFAULT_MAX_FRAMING_BUFFER_SIZE = 1048576; // 1MB
|
||||
private int maxFramingBufferSize = DEFAULT_MAX_FRAMING_BUFFER_SIZE;
|
||||
|
||||
public PrinterConfig() {
|
||||
initDefaultChannelTable();
|
||||
@@ -227,4 +229,9 @@ public class PrinterConfig {
|
||||
public int[] getChannelTable() {
|
||||
return channelTable.clone();
|
||||
}
|
||||
|
||||
public int getMaxFramingBufferSize() { return maxFramingBufferSize; }
|
||||
public void setMaxFramingBufferSize(int maxFramingBufferSize) {
|
||||
this.maxFramingBufferSize = maxFramingBufferSize > 0 ? maxFramingBufferSize : DEFAULT_MAX_FRAMING_BUFFER_SIZE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,12 +58,17 @@ public class Telnet3270EP implements Runnable {
|
||||
private int lastResponseRequired = TN3270EConstants.RSF_NO_RESPONSE;
|
||||
|
||||
// Buffer for Telnet streaming
|
||||
private final ByteArrayOutputStream recordBuffer = new ByteArrayOutputStream();
|
||||
private static final int MAX_PRINTER_SUBNEG_SIZE = 8192;
|
||||
private final int maxFramingBufferSize;
|
||||
private boolean recordDiscardMode = false;
|
||||
private final ByteArrayOutputStream recordBuffer;
|
||||
private final ByteArrayOutputStream subnegBuffer = new ByteArrayOutputStream();
|
||||
private boolean inSubnegotiation = false;
|
||||
|
||||
public Telnet3270EP(PrinterConfig config) {
|
||||
this.config = config != null ? config : new PrinterConfig();
|
||||
this.maxFramingBufferSize = this.config.getMaxFramingBufferSize();
|
||||
this.recordBuffer = new ByteArrayOutputStream(Math.min(32768, this.maxFramingBufferSize));
|
||||
this.pd = new PD3270(this.config);
|
||||
this.translator = new EbcdicTranslator(this.config.getCodePage());
|
||||
this.scs = new PrintSCS3270(this.config, this.pd, this.translator);
|
||||
@@ -73,6 +78,8 @@ public class Telnet3270EP implements Runnable {
|
||||
|
||||
public Telnet3270EP(PrinterConfig config, PD3270 pd, EbcdicTranslator translator) {
|
||||
this.config = config != null ? config : new PrinterConfig();
|
||||
this.maxFramingBufferSize = this.config.getMaxFramingBufferSize();
|
||||
this.recordBuffer = new ByteArrayOutputStream(Math.min(32768, this.maxFramingBufferSize));
|
||||
this.pd = pd != null ? pd : new PD3270(this.config);
|
||||
this.translator = translator != null ? translator : new EbcdicTranslator(this.config.getCodePage());
|
||||
this.scs = new PrintSCS3270(this.config, this.pd, this.translator);
|
||||
@@ -191,6 +198,7 @@ public class Telnet3270EP implements Runnable {
|
||||
this.recordBuffer.reset();
|
||||
this.subnegBuffer.reset();
|
||||
this.inSubnegotiation = false;
|
||||
this.recordDiscardMode = false;
|
||||
|
||||
scs.resetSCSFormatDefaults();
|
||||
printPs.erasePrintBuffer();
|
||||
@@ -379,11 +387,19 @@ public class Telnet3270EP implements Runnable {
|
||||
processSubnegotiation(subnegBuffer.toByteArray());
|
||||
subnegBuffer.reset();
|
||||
} else if (idx < end && (data[idx] & 0xFF) == TelnetConstants.IAC) {
|
||||
subnegBuffer.write(TelnetConstants.IAC);
|
||||
if (subnegBuffer.size() < MAX_PRINTER_SUBNEG_SIZE) {
|
||||
subnegBuffer.write(TelnetConstants.IAC);
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
} else {
|
||||
subnegBuffer.write(b);
|
||||
if (subnegBuffer.size() < MAX_PRINTER_SUBNEG_SIZE) {
|
||||
subnegBuffer.write(b);
|
||||
} else {
|
||||
log.warning("Printer subnegotiation exceeded " + MAX_PRINTER_SUBNEG_SIZE + " bytes; aborting SB");
|
||||
subnegBuffer.reset();
|
||||
inSubnegotiation = false;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -394,15 +410,18 @@ public class Telnet3270EP implements Runnable {
|
||||
|
||||
switch (cmd) {
|
||||
case TelnetConstants.IAC:
|
||||
recordBuffer.write(TelnetConstants.IAC);
|
||||
appendPrinterByte(TelnetConstants.IAC);
|
||||
break;
|
||||
case TelnetConstants.SB:
|
||||
inSubnegotiation = true;
|
||||
subnegBuffer.reset();
|
||||
break;
|
||||
case TelnetConstants.EOR:
|
||||
processCompleteRecord(recordBuffer.toByteArray());
|
||||
if (!recordDiscardMode) {
|
||||
processCompleteRecord(recordBuffer.toByteArray());
|
||||
}
|
||||
recordBuffer.reset();
|
||||
recordDiscardMode = false;
|
||||
break;
|
||||
case TelnetConstants.DO:
|
||||
if (idx < end) handleTelnetDo(data[idx++] & 0xFF);
|
||||
@@ -420,11 +439,33 @@ public class Telnet3270EP implements Runnable {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
recordBuffer.write(b);
|
||||
appendPrinterByte(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void appendPrinterByte(int b) {
|
||||
if (recordDiscardMode) {
|
||||
return;
|
||||
}
|
||||
if (recordBuffer.size() >= maxFramingBufferSize) {
|
||||
log.warning("Printer record buffer exceeded maximum capacity (" + maxFramingBufferSize +
|
||||
" bytes) without IAC EOR; discarding until next EOR");
|
||||
recordDiscardMode = true;
|
||||
recordBuffer.reset();
|
||||
return;
|
||||
}
|
||||
recordBuffer.write(b);
|
||||
}
|
||||
|
||||
public boolean isRecordDiscardMode() {
|
||||
return recordDiscardMode;
|
||||
}
|
||||
|
||||
public int getMaxFramingBufferSize() {
|
||||
return maxFramingBufferSize;
|
||||
}
|
||||
|
||||
private void handleTelnetDo(int option) {
|
||||
try {
|
||||
if (option == TelnetConstants.TELOPT_TN3270E) {
|
||||
|
||||
@@ -46,8 +46,14 @@ public class TelnetFSM {
|
||||
private final boolean[] myOpts = new boolean[256]; // options we have enabled
|
||||
private final boolean[] hisOpts = new boolean[256]; // options the host has enabled
|
||||
|
||||
// 3270 input buffer (accumulated between telnet framing)
|
||||
private final haus.nightmare.lib3270j.datastream.FastByteBuffer ibuf = new haus.nightmare.lib3270j.datastream.FastByteBuffer(32768);
|
||||
// 3270 input buffer (accumulated between telnet framing, bounded at maxFramingBufferSize)
|
||||
private final haus.nightmare.lib3270j.datastream.FastByteBuffer ibuf;
|
||||
|
||||
// Discard mode if framing buffer overflows prior to receiving IAC EOR
|
||||
private boolean recordDiscardMode = false;
|
||||
|
||||
// Sanity cap for Telnet subnegotiation parameters (RFC 1091, RFC 1572, RFC 2355)
|
||||
private static final int MAX_SUBNEG_BUFFER_SIZE = 8192;
|
||||
|
||||
// Reusable single-byte scratch buffer for plain NVT data dispatch (Phase 27.5 / T1-C)
|
||||
private final byte[] nvtSingleByteScratch = new byte[1];
|
||||
@@ -165,6 +171,8 @@ public class TelnetFSM {
|
||||
this.config = config;
|
||||
this.screenBuffer = screenBuffer;
|
||||
this.dsProcessor = dsProcessor;
|
||||
int maxFraming = (config != null) ? config.getMaxFramingBufferSize() : ConnectionConfig.DEFAULT_MAX_FRAMING_BUFFER_SIZE;
|
||||
this.ibuf = new haus.nightmare.lib3270j.datastream.FastByteBuffer(Math.min(32768, maxFraming), maxFraming);
|
||||
this.nvtProcessor = new haus.nightmare.lib3270j.nvt.NvtProcessor(screenBuffer, new haus.nightmare.lib3270j.charset.EbcdicTranslator());
|
||||
this.nvtProcessor.setOutputSender(this::sendNvtData);
|
||||
}
|
||||
@@ -220,6 +228,7 @@ public class TelnetFSM {
|
||||
deferredWillTtype = false;
|
||||
ttypeIndex = 0;
|
||||
luIndex = 0;
|
||||
recordDiscardMode = false;
|
||||
ibuf.reset();
|
||||
sbbuf.reset();
|
||||
|
||||
@@ -256,7 +265,7 @@ public class TelnetFSM {
|
||||
if (isPlainNvt) {
|
||||
nvtProcessor.processNVTData(buf, start, i - start);
|
||||
} else if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING || hisOpts[TELOPT_BINARY] || hisOpts[TELOPT_EOR]) {
|
||||
ibuf.write(buf, start, i - start);
|
||||
appendInboundData(buf, start, i - start);
|
||||
}
|
||||
}
|
||||
if (i < end) {
|
||||
@@ -334,24 +343,74 @@ public class TelnetFSM {
|
||||
|
||||
// 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);
|
||||
appendInboundByte(c);
|
||||
}
|
||||
}
|
||||
|
||||
private void appendInboundByte(int c) {
|
||||
if (recordDiscardMode) {
|
||||
return;
|
||||
}
|
||||
if (ibuf.size() >= ibuf.getMaxCapacity()) {
|
||||
handleFramingOverflow();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
ibuf.write(c);
|
||||
} catch (haus.nightmare.lib3270j.datastream.FramingBufferOverflowException e) {
|
||||
handleFramingOverflow();
|
||||
}
|
||||
}
|
||||
|
||||
private void appendInboundData(byte[] buf, int offset, int len) {
|
||||
if (recordDiscardMode || buf == null || len <= 0) {
|
||||
return;
|
||||
}
|
||||
if (ibuf.size() + len > ibuf.getMaxCapacity()) {
|
||||
handleFramingOverflow();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
ibuf.write(buf, offset, len);
|
||||
} catch (haus.nightmare.lib3270j.datastream.FramingBufferOverflowException e) {
|
||||
handleFramingOverflow();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleFramingOverflow() {
|
||||
log.warning("Inbound 3270 framing buffer exceeded maximum capacity (" + ibuf.getMaxCapacity() +
|
||||
" bytes) without IAC EOR; discarding record until next EOR");
|
||||
recordDiscardMode = true;
|
||||
ibuf.reset();
|
||||
}
|
||||
|
||||
public boolean isRecordDiscardMode() {
|
||||
return recordDiscardMode;
|
||||
}
|
||||
|
||||
public int getMaxFramingBufferSize() {
|
||||
return ibuf.getMaxCapacity();
|
||||
}
|
||||
|
||||
public int getCurrentFramingBufferSize() {
|
||||
return ibuf.size();
|
||||
}
|
||||
|
||||
// ========== TNS_IAC ==========
|
||||
|
||||
private void processIAC(int c) {
|
||||
switch (c) {
|
||||
case IAC: // Escaped IAC — literal 0xFF
|
||||
ibuf.write(c);
|
||||
appendInboundByte(c);
|
||||
state = TNS_DATA;
|
||||
break;
|
||||
case EOR: // End of record — process accumulated 3270 data
|
||||
log.fine("RCVD EOR");
|
||||
if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING) {
|
||||
if (!recordDiscardMode && (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING)) {
|
||||
processEndOfRecord();
|
||||
}
|
||||
ibuf.reset();
|
||||
recordDiscardMode = false;
|
||||
state = TNS_DATA;
|
||||
break;
|
||||
case WILL: state = TNS_WILL; break;
|
||||
@@ -527,7 +586,14 @@ public class TelnetFSM {
|
||||
if (c == IAC) {
|
||||
state = TNS_SB_IAC;
|
||||
} else {
|
||||
sbbuf.write(c);
|
||||
if (sbbuf.size() < MAX_SUBNEG_BUFFER_SIZE) {
|
||||
sbbuf.write(c);
|
||||
} else {
|
||||
log.warning("Telnet subnegotiation exceeded " + MAX_SUBNEG_BUFFER_SIZE +
|
||||
" bytes without IAC SE; discarding and aborting SB");
|
||||
sbbuf.reset();
|
||||
state = TNS_DATA;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -542,7 +608,9 @@ public class TelnetFSM {
|
||||
state = TNS_DATA;
|
||||
} else if (c == IAC) {
|
||||
// Escaped IAC within sub-negotiation
|
||||
sbbuf.write(IAC);
|
||||
if (sbbuf.size() < MAX_SUBNEG_BUFFER_SIZE) {
|
||||
sbbuf.write(IAC);
|
||||
}
|
||||
state = TNS_SB;
|
||||
} else {
|
||||
// Shouldn't happen, but recover
|
||||
@@ -1634,6 +1702,7 @@ public class TelnetFSM {
|
||||
state = TNS_DATA;
|
||||
java.util.Arrays.fill(myOpts, false);
|
||||
java.util.Arrays.fill(hisOpts, false);
|
||||
recordDiscardMode = false;
|
||||
ibuf.reset();
|
||||
sbbuf.reset();
|
||||
tn3270eNegotiated = false;
|
||||
|
||||
@@ -151,4 +151,39 @@ public class FastByteBufferTest {
|
||||
executor.shutdown();
|
||||
assertEquals(0, failures.get(), "No failures occurred during concurrent pool operations");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("FastByteBuffer enforces maxCapacity and throws FramingBufferOverflowException")
|
||||
public void testFastByteBufferMaxCapacityEnforcement() {
|
||||
FastByteBuffer buf = new FastByteBuffer(64, 128);
|
||||
assertEquals(128, buf.getMaxCapacity());
|
||||
assertEquals(64, buf.getInitialCapacity());
|
||||
|
||||
byte[] chunk = new byte[64];
|
||||
buf.write(chunk, 0, 64);
|
||||
assertEquals(64, buf.size());
|
||||
|
||||
buf.write(chunk, 0, 64);
|
||||
assertEquals(128, buf.size());
|
||||
|
||||
// Writing one more byte must throw FramingBufferOverflowException
|
||||
assertThrows(FramingBufferOverflowException.class, () -> buf.write(0xFF));
|
||||
|
||||
// Writing a chunk that exceeds capacity must also throw
|
||||
assertThrows(FramingBufferOverflowException.class, () -> buf.write(chunk, 0, 10));
|
||||
|
||||
// After reset, buffer is ready for use again
|
||||
buf.reset();
|
||||
assertEquals(0, buf.size());
|
||||
buf.write(chunk, 0, 64);
|
||||
assertEquals(64, buf.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("FastByteBuffer default maxCapacity is 1048576 bytes (1MB)")
|
||||
public void testDefaultMaxCapacity() {
|
||||
FastByteBuffer buf = new FastByteBuffer();
|
||||
assertEquals(FastByteBuffer.DEFAULT_MAX_CAPACITY, buf.getMaxCapacity());
|
||||
assertEquals(1048576, buf.getMaxCapacity());
|
||||
}
|
||||
}
|
||||
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
package haus.nightmare.lib3270j.telnet;
|
||||
|
||||
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.nvt.NvtProcessor;
|
||||
import haus.nightmare.lib3270j.printer.PrinterConfig;
|
||||
import haus.nightmare.lib3270j.printer.Telnet3270EP;
|
||||
import haus.nightmare.lib3270j.protocol.TelnetConstants;
|
||||
import haus.nightmare.lib3270j.screen.ScreenBuffer;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static haus.nightmare.lib3270j.protocol.TelnetConstants.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class FramingBufferBoundedCapTest {
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
private void feedBytes(int... bytes) {
|
||||
byte[] b = new byte[bytes.length];
|
||||
for (int i = 0; i < bytes.length; i++) {
|
||||
b[i] = (byte) bytes[i];
|
||||
}
|
||||
fsm.feedBytes(b, 0, b.length);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Framing buffer caps at 1MB and enters discard mode if stream lacks IAC EOR")
|
||||
public void testFramingBufferOverflowDiscardsUntilEor() {
|
||||
fsm.onConnected();
|
||||
|
||||
// Negotiate BINARY and EOR to establish standard 3270 streaming regime
|
||||
feedBytes(IAC, DO, TELOPT_EOR);
|
||||
feedBytes(IAC, WILL, TELOPT_EOR);
|
||||
feedBytes(IAC, DO, TELOPT_BINARY);
|
||||
feedBytes(IAC, WILL, TELOPT_BINARY);
|
||||
|
||||
assertEquals(1048576, fsm.getMaxFramingBufferSize(), "Default framing buffer cap should be 1MB (1048576 bytes)");
|
||||
assertFalse(fsm.isRecordDiscardMode());
|
||||
|
||||
// Feed chunks totaling over 1MB without any IAC EOR delimiter
|
||||
byte[] chunk = new byte[8192];
|
||||
Arrays.fill(chunk, (byte) 0xC1); // EBCDIC 'A'
|
||||
|
||||
for (int i = 0; i < 128; i++) {
|
||||
fsm.feedBytes(chunk, 0, chunk.length); // 128 * 8192 = 1,048,576 bytes
|
||||
}
|
||||
assertFalse(fsm.isRecordDiscardMode(), "Buffer should be at exact cap without overflowing yet");
|
||||
|
||||
// Now feed an additional chunk exceeding the 1MB cap without IAC EOR
|
||||
fsm.feedBytes(chunk, 0, 100);
|
||||
|
||||
assertTrue(fsm.isRecordDiscardMode(), "FSM must enter recordDiscardMode when cap is exceeded without IAC EOR");
|
||||
assertEquals(0, fsm.getCurrentFramingBufferSize(), "Buffer must be reset to free memory upon overflow");
|
||||
|
||||
// Further incoming data without IAC EOR must be discarded without growing the buffer
|
||||
fsm.feedBytes(chunk, 0, chunk.length);
|
||||
assertEquals(0, fsm.getCurrentFramingBufferSize(), "Discard mode must drop subsequent data without accumulating memory");
|
||||
assertTrue(fsm.isRecordDiscardMode());
|
||||
|
||||
// Feeding IAC EOR must clear discard mode and reset buffer cleanly
|
||||
feedBytes(IAC, EOR);
|
||||
assertFalse(fsm.isRecordDiscardMode(), "IAC EOR must clear recordDiscardMode");
|
||||
assertEquals(0, fsm.getCurrentFramingBufferSize());
|
||||
|
||||
// A subsequent valid record must be processed normally
|
||||
// 0xF5 (Erase/Write), 0xC2 (WCC: unlock keyboard), 0x11 (SBA), 0x00, 0x00, 0xC1 ('A'), IAC, EOR
|
||||
byte[] valid3270 = new byte[] { (byte) 0xF5, (byte) 0xC2, (byte) 0x11, 0x00, 0x00, (byte) 0xC1, (byte) IAC, (byte) EOR };
|
||||
fsm.feedBytes(valid3270, 0, valid3270.length);
|
||||
|
||||
assertEquals('A', screenBuffer.getDisplayCell(0).ucs4);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Custom maxFramingBufferSize on ConnectionConfig is respected by TelnetFSM")
|
||||
public void testCustomMaxFramingBufferSize() {
|
||||
ConnectionConfig customConfig = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
|
||||
customConfig.setMaxFramingBufferSize(1024); // 1KB custom limit
|
||||
|
||||
TelnetFSM customFsm = new TelnetFSM(customConfig, screenBuffer, dsProcessor);
|
||||
customFsm.setConnection(connection);
|
||||
customFsm.onConnected();
|
||||
|
||||
assertEquals(1024, customFsm.getMaxFramingBufferSize());
|
||||
|
||||
// Negotiate BINARY and EOR
|
||||
byte[] neg = new byte[] {
|
||||
(byte) IAC, (byte) DO, (byte) TELOPT_EOR,
|
||||
(byte) IAC, (byte) WILL, (byte) TELOPT_EOR,
|
||||
(byte) IAC, (byte) DO, (byte) TELOPT_BINARY,
|
||||
(byte) IAC, (byte) WILL, (byte) TELOPT_BINARY
|
||||
};
|
||||
customFsm.feedBytes(neg, 0, neg.length);
|
||||
|
||||
byte[] payload500 = new byte[500];
|
||||
Arrays.fill(payload500, (byte) 0xC1);
|
||||
customFsm.feedBytes(payload500, 0, 500);
|
||||
assertEquals(500, customFsm.getCurrentFramingBufferSize());
|
||||
assertFalse(customFsm.isRecordDiscardMode());
|
||||
|
||||
customFsm.feedBytes(payload500, 0, 500);
|
||||
assertEquals(1000, customFsm.getCurrentFramingBufferSize());
|
||||
assertFalse(customFsm.isRecordDiscardMode());
|
||||
|
||||
// Overflow over 1024 limit
|
||||
customFsm.feedBytes(payload500, 0, 100);
|
||||
assertTrue(customFsm.isRecordDiscardMode());
|
||||
assertEquals(0, customFsm.getCurrentFramingBufferSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Telnet subnegotiation buffer caps at 8KB if stream lacks IAC SE")
|
||||
public void testSubnegotiationBufferCap() {
|
||||
fsm.onConnected();
|
||||
|
||||
// Begin subnegotiation: IAC SB TELOPT_TTYPE
|
||||
feedBytes(IAC, SB, TELOPT_TTYPE);
|
||||
|
||||
// Feed > 8192 bytes without IAC SE
|
||||
byte[] largeSb = new byte[8200];
|
||||
Arrays.fill(largeSb, (byte) 'X');
|
||||
fsm.feedBytes(largeSb, 0, largeSb.length);
|
||||
|
||||
// Feed a regular byte: should be back in TNS_DATA, not accumulating in SB
|
||||
feedBytes(IAC, NOP);
|
||||
// Ensure no exception and session remains operational
|
||||
assertTrue(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Printer framing buffer caps at 1MB and enters discard mode without IAC EOR")
|
||||
public void testPrinterFramingBufferCap() throws Exception {
|
||||
PrinterConfig pcfg = new PrinterConfig("localhost", 23);
|
||||
assertEquals(1048576, pcfg.getMaxFramingBufferSize());
|
||||
|
||||
Telnet3270EP printer = new Telnet3270EP(pcfg);
|
||||
assertEquals(1048576, printer.getMaxFramingBufferSize());
|
||||
assertFalse(printer.isRecordDiscardMode());
|
||||
|
||||
Method processIncoming = Telnet3270EP.class.getDeclaredMethod("processIncomingBytes", byte[].class, int.class, int.class);
|
||||
processIncoming.setAccessible(true);
|
||||
|
||||
byte[] chunk = new byte[32768];
|
||||
Arrays.fill(chunk, (byte) 0x40);
|
||||
|
||||
for (int i = 0; i < 33; i++) {
|
||||
processIncoming.invoke(printer, chunk, 0, chunk.length); // 33 * 32768 = 1,081,344 bytes > 1,048,576
|
||||
}
|
||||
|
||||
assertTrue(printer.isRecordDiscardMode(), "Printer must enter discard mode when record exceeds 1MB");
|
||||
|
||||
// Send IAC EOR
|
||||
byte[] eor = new byte[] { (byte) IAC, (byte) EOR };
|
||||
processIncoming.invoke(printer, eor, 0, eor.length);
|
||||
|
||||
assertFalse(printer.isRecordDiscardMode(), "Printer must clear discard mode on IAC EOR");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("NvtProcessor caps escape sequence buffer at 4KB if terminator is missing")
|
||||
public void testNvtProcessorEscapeBufferCap() {
|
||||
NvtProcessor nvt = fsm.getNvtProcessor();
|
||||
|
||||
// Stream ESC [ followed by 5000 digits without terminating byte (0x40..0x7E)
|
||||
byte[] escStart = new byte[] { 0x1B, '[' };
|
||||
nvt.processNVTData(escStart, 0, escStart.length);
|
||||
|
||||
byte[] digits = new byte[5000];
|
||||
Arrays.fill(digits, (byte) '1');
|
||||
nvt.processNVTData(digits, 0, digits.length);
|
||||
|
||||
// After escBuffer reaches 4096 bytes, it aborts escape parsing and returns to STATE_NORMAL.
|
||||
// Subsequent digits are rendered as plain text in the screen buffer starting at cell 0.
|
||||
assertEquals('1', screenBuffer.getCell(0).ucs4);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user