Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ var targets: [Target] = [
.target(
name: "MTHCore",
dependencies: [
.target(name: "CZlib", condition: .when(platforms: [.macOS, .linux, .iOS, .visionOS])),
"CZlib",
],
path: "Sources/SwiftMTHCore"
),
Expand Down
7 changes: 6 additions & 1 deletion Sources/SwiftMTHClient/TelnetClientSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,11 @@ public final class TelnetClientSession {
TeloptPattern(pattern: [TC.IAC, TC.DO, TO.NAWS],
handler: { s, _, _, _ in s.processDoNaws(); return 3 }),

// MCCP1 (option 85) uses a non-standard SB terminator: IAC SB 85 WILL SE
// where SE appears without a preceding IAC. Skip the 5-byte start sequence.
TeloptPattern(pattern: [TC.IAC, TC.SB, TO.MCCP1, TC.WILL, TC.SE],
handler: { _, _, _, _ in return 5 }),

// Prompt markers
TeloptPattern(pattern: [TC.IAC, TC.EOR],
handler: { s, _, _, _ in s.delegate?.onPromptReceived(); return 2 }),
Expand Down Expand Up @@ -339,7 +344,7 @@ public final class TelnetClientSession {

private func processSbMccp2() {
guard let stream = InflateStream() else {
log("MCCP2: Failed to initialize inflate stream.")
log("MCCP2: Failed to initialize inflate stream. InflateStream() returned nil.")
return
}
mccp2 = stream
Expand Down
2 changes: 2 additions & 0 deletions Sources/SwiftMTHCore/TelnetConstants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public enum TelnetOption {
public static let CHARSET: UInt8 = 42
public static let MSDP: UInt8 = 69
public static let MSSP: UInt8 = 70
public static let MCCP1: UInt8 = 85
public static let MCCP2: UInt8 = 86
public static let MCCP3: UInt8 = 87
public static let MSP: UInt8 = 90
Expand Down Expand Up @@ -104,6 +105,7 @@ public let defaultTelnetTable: [TelnetOptionEntry] = {
table[42] = TelnetOptionEntry("CHARSET", .will)
table[69] = TelnetOptionEntry("MSDP", .will)
table[70] = TelnetOptionEntry("MSSP", .will)
table[85] = TelnetOptionEntry("MCCP1")
table[86] = TelnetOptionEntry("MCCP2", .will)
table[87] = TelnetOptionEntry("MCCP3", .will)
table[90] = TelnetOptionEntry("MSP", .will)
Expand Down
141 changes: 141 additions & 0 deletions Tests/MTHTests/TelnetClientSessionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -400,4 +400,145 @@ struct TelnetClientSessionTests {
#expect(d.msspData.count == 1)
#expect(d.msspData[0]["GENRE"] == "Adventure")
}

// MARK: - Nanvaent Replay Tests

/// Captured bytes from nanvaent.org:23 for replay testing.
/// These exercise MCCP2 with standard zlib (windowBits=15, header 78 DA),
/// embedded telnet commands in compressed stream, and unknown option handling.
private enum Nanvaent {
// Round 1: IAC DO TTYPE
static let round1: [UInt8] = [0xFF, 0xFD, 0x18]

// Round 2: DO NAWS, WILL MCCP2, DO MXP(91), WILL MSSP, WILL 93, DO NEW_ENVIRON
static let round2: [UInt8] = [
0xFF, 0xFD, 0x1F, 0xFF, 0xFB, 0x56, 0xFF, 0xFD, 0x5B, 0xFF, 0xFB, 0x46, 0xFF, 0xFB, 0x5D, 0xFF,
0xFD, 0x27,
]

// Round 3: SB TTYPE SEND IAC SE
static let round3: [UInt8] = [0xFF, 0xFA, 0x18, 0x01, 0xFF, 0xF0]

// Round 4: SB MCCP2 IAC SE (compression starts)
static let round4: [UInt8] = [0xFF, 0xFA, 0x56, 0xFF, 0xF0]

// Round 5: compressed data — MSSP subneg + unknown option SBs (standard zlib, 78 DA header)
static let round5: [UInt8] = [
0x78, 0xDA, 0xFA, 0xFF, 0xCB, 0x0D, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x62, 0xF4, 0x73, 0xF4, 0x75,
0x65, 0xF2, 0x4B, 0xCC, 0x2B, 0x4B, 0x4C, 0xCD, 0x2B, 0x01, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x62,
0x0C, 0xF0, 0x71, 0x8C, 0x74, 0x0D, 0x0A, 0x66, 0xB2, 0x04, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x62,
0x0C, 0x0D, 0x08, 0xF1, 0x04, 0xCA, 0x18, 0x9A, 0x9B, 0x1B, 0x5A, 0x98, 0x5B, 0x9A, 0x1A, 0x1A,
0x01, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFA, 0xFF, 0x01, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFA, 0xFF,
0x2B, 0xF4, 0xF7, 0x07, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF,
]

// Round 6: compressed banner text (370 bytes)
static let round6: [UInt8] = [
0xB4, 0x53, 0x3D, 0x6F, 0xC3, 0x20, 0x10, 0xDD, 0x2D, 0xF9, 0x3F, 0x9C, 0xB2, 0xA4, 0x1D, 0x12,
0xF6, 0x48, 0x91, 0xDA, 0xC1, 0x5B, 0x87, 0x48, 0x55, 0x87, 0xAA, 0x54, 0x16, 0x49, 0x88, 0x4D,
0x6B, 0x43, 0x64, 0x83, 0x2A, 0xFF, 0xFB, 0xDE, 0x61, 0x9C, 0x92, 0xC4, 0xAD, 0xBB, 0xE4, 0x84,
0x6C, 0xEE, 0xE3, 0x3D, 0xB8, 0x07, 0xA4, 0x09, 0xBC, 0xAD, 0xD7, 0xEB, 0xC5, 0x99, 0x61, 0xE0,
0x1D, 0xB2, 0x45, 0x2D, 0x54, 0x05, 0x2B, 0xD0, 0x81, 0xED, 0x61, 0x98, 0x2C, 0x4D, 0x53, 0xFC,
0x82, 0x4A, 0x13, 0x24, 0x9C, 0x32, 0xC6, 0xFD, 0x6F, 0xBA, 0x92, 0x01, 0xF0, 0xEB, 0x68, 0x9E,
0x4F, 0x22, 0x19, 0xAD, 0xC1, 0xC7, 0xE8, 0xA6, 0xA0, 0xAC, 0x5F, 0xF4, 0x12, 0x8B, 0x3E, 0x9B,
0x80, 0x7A, 0xE4, 0x08, 0x96, 0xF5, 0xC8, 0x3C, 0x32, 0xEF, 0x84, 0x10, 0x16, 0xE4, 0xAC, 0x77,
0x08, 0x1A, 0x65, 0x10, 0x78, 0x05, 0xF2, 0x86, 0x74, 0x77, 0x71, 0x62, 0x58, 0x97, 0xE6, 0xBC,
0x77, 0x42, 0x13, 0xA7, 0x50, 0x0E, 0x43, 0x86, 0xE8, 0x91, 0x2A, 0xA2, 0xBE, 0x0F, 0x8D, 0x9D,
0x38, 0xB9, 0xEF, 0x77, 0x80, 0x0D, 0x83, 0xE8, 0x4E, 0x21, 0xDA, 0xDE, 0x90, 0xF1, 0x9F, 0x0B,
0xAE, 0x1F, 0x3E, 0x4E, 0x83, 0xF1, 0xBE, 0xA3, 0xC8, 0x89, 0x42, 0xD4, 0x6A, 0x9F, 0xA1, 0xFD,
0xB1, 0x4B, 0x9E, 0x71, 0x9D, 0xE3, 0xB3, 0xE1, 0x91, 0xCE, 0xFF, 0xC3, 0x04, 0x54, 0xC0, 0xBC,
0xB4, 0x12, 0xE6, 0x85, 0x93, 0xAD, 0x9D, 0x83, 0x3A, 0xFC, 0x71, 0xF7, 0xBC, 0x04, 0xCC, 0xDF,
0xDC, 0xCE, 0x38, 0xF8, 0x70, 0xAD, 0x85, 0x2F, 0xA1, 0x2D, 0x58, 0x13, 0xC4, 0x65, 0x30, 0x02,
0x22, 0x32, 0xC2, 0x54, 0xC6, 0x7C, 0x82, 0x68, 0x8C, 0xD3, 0xFB, 0xE5, 0xC4, 0x0D, 0xF6, 0xA7,
0x15, 0x5E, 0x93, 0x95, 0x95, 0x96, 0x16, 0xCE, 0x9E, 0xDF, 0xA4, 0x95, 0xD6, 0x1E, 0x57, 0x8C,
0xC5, 0x20, 0x76, 0x83, 0xC7, 0xFE, 0x6A, 0x5C, 0x03, 0x8D, 0x2A, 0x4A, 0xDB, 0x42, 0x2D, 0x3A,
0xD8, 0x4A, 0xD8, 0xAB, 0x76, 0x57, 0x09, 0x55, 0x4B, 0x6C, 0x72, 0x53, 0x49, 0x81, 0xE2, 0xDA,
0xEE, 0x28, 0x61, 0x56, 0xCA, 0xEA, 0x18, 0x6A, 0x67, 0x20, 0x0E, 0x56, 0x36, 0xA8, 0x48, 0xA1,
0xF4, 0x32, 0x4D, 0x36, 0x4F, 0xD9, 0xE3, 0x73, 0x06, 0x8E, 0x6A, 0x4B, 0x3C, 0x8C, 0xAD, 0x2B,
0xE6, 0xB0, 0x33, 0x75, 0x2D, 0xF4, 0x9E, 0xC4, 0xC5, 0x3A, 0xC0, 0x58, 0xEB, 0x65, 0x3F, 0x28,
0xD4, 0x2F, 0x4D, 0x32, 0x4D, 0x0C, 0x1D, 0x6D, 0x40, 0x8B, 0x5A, 0xAE, 0xE0, 0x1B, 0x00, 0x00,
0xFF, 0xFF,
]
}

#if canImport(CZlib)
@Test func nanvaentNegotiationAndMCCP2() {
let (s, d) = makeSession()
s.terminalType = "Wammer"

// Round 1: DO TTYPE
_ = s.processInput(Nanvaent.round1)
#expect(d.allWrittenBytes.containsSequence([TC.IAC, TC.WILL, TO.TTYPE]))

// Round 2: DO NAWS, WILL MCCP2, DO MXP(91), WILL MSSP, WILL 93, DO NEW_ENVIRON
d.writtenChunks.removeAll()
_ = s.processInput(Nanvaent.round2)
let r2 = d.allWrittenBytes
#expect(r2.containsSequence([TC.IAC, TC.WILL, TO.NAWS])) // WILL NAWS
#expect(r2.containsSequence([TC.IAC, TC.DO, TO.MCCP2])) // DO MCCP2
#expect(r2.containsSequence([TC.IAC, TC.WONT, 91])) // WONT MXP
#expect(r2.containsSequence([TC.IAC, TC.DO, TO.MSSP])) // DO MSSP
#expect(r2.containsSequence([TC.IAC, TC.DONT, 93])) // DONT unknown(93)
#expect(r2.containsSequence([TC.IAC, TC.WONT, 39])) // WONT NEW_ENVIRON

// Round 3: SB TTYPE SEND
d.writtenChunks.removeAll()
_ = s.processInput(Nanvaent.round3)
let ttypeResponse: [UInt8] = [TC.IAC, TC.SB, TO.TTYPE, TS.ENV_IS] + Array("Wammer".utf8) + [TC.IAC, TC.SE]
#expect(d.allWrittenBytes == ttypeResponse)

// Round 4: SB MCCP2 (compression starts, no trailing data)
d.writtenChunks.removeAll()
_ = s.processInput(Nanvaent.round4)
#expect(s.isMCCP2Active)

// Round 5: compressed telnet data (MSSP + unknown SBs)
let out5 = s.processInput(Nanvaent.round5)
#expect(d.msspData.count == 1, "MSSP data should be received from compressed stream")
#expect(d.msspData[0]["NAME"] == "Nanvaent")
// Output should be empty (only telnet commands, no visible text)
#expect(out5.isEmpty, "Round 5 should contain only telnet commands, no visible text")

// Round 6: compressed banner text
let out6 = s.processInput(Nanvaent.round6)
let bannerText = String(decoding: out6, as: UTF8.self)
#expect(!out6.isEmpty)
#expect(bannerText.contains("Enter your name:"))
#expect(bannerText.contains("nanvaent.org"))
}

@Test func nanvaentInflateStreamDirectly() throws {
let inflater = try #require(InflateStream())

let r5 = try #require(inflater.decompress(Nanvaent.round5))
#expect(!r5.decompressed.isEmpty)

let r6 = try #require(inflater.decompress(Nanvaent.round6))
let text = String(decoding: r6.decompressed, as: UTF8.self)
#expect(text.contains("nanvaent"))
}

@Test func nanvaentCombinedMCCP2Packet() {
// Test the case where MCCP2 SB and compressed data arrive in one packet
let (s, d) = makeSession()
s.terminalType = "Wammer"

_ = s.processInput(Nanvaent.round1)
_ = s.processInput(Nanvaent.round2)
_ = s.processInput(Nanvaent.round3)

// Combine rounds 4+5 into one packet (MCCP2 SB + compressed data together)
let combined = Nanvaent.round4 + Nanvaent.round5
_ = s.processInput(combined)
#expect(s.isMCCP2Active)
#expect(d.msspData.count == 1)
#expect(d.msspData[0]["NAME"] == "Nanvaent")

// Banner still arrives in next packet
let out6 = s.processInput(Nanvaent.round6)
let bannerText = String(decoding: out6, as: UTF8.self)
#expect(bannerText.contains("Enter your name:"))
}

#endif
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ object TelnetOption {
const val CHARSET: Byte = 42
const val MSDP: Byte = 69
const val MSSP: Byte = 70
const val MCCP1: Byte = 85.toByte()
const val MCCP2: Byte = 86.toByte()
const val MCCP3: Byte = 87.toByte()
const val MSP: Byte = 90.toByte()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,11 @@ class TelnetClientSession(
TeloptPattern(byteArrayOf(TC.IAC, TC.DO, TO.NAWS))
{ s, _, _, _ -> s.processDoNaws(); 3 },

// MCCP1 (option 85) uses a non-standard SB terminator: IAC SB 85 WILL SE
// where SE appears without a preceding IAC. Skip the 5-byte start sequence.
TeloptPattern(byteArrayOf(TC.IAC, TC.SB, TO.MCCP1, TC.WILL, TC.SE))
{ _, _, _, _ -> 5 },

// EOR command (prompt marker)
TeloptPattern(byteArrayOf(TC.IAC, TC.EOR))
{ s, _, _, _ -> s.processEorCommand(); 2 },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ private const val EOR_OPT: Byte = 25
private const val NAWS: Byte = 31
private const val MSDP: Byte = 69
private const val MSSP: Byte = 70
private const val MCCP1: Byte = 85
private const val MCCP2: Byte = 86
private const val GMCP: Byte = 0xC9.toByte()

Expand Down Expand Up @@ -523,4 +524,119 @@ class TelnetClientSessionTest {
assertEquals(1, d.msspData.size)
assertEquals("Adventure", d.msspData[0]["GENRE"])
}

// -- MCCP1 non-standard SB terminator --

@Test fun mccp1StartSequenceSkipped() {
// MCCP1 uses IAC SB 85 WILL SE (non-standard: SE without preceding IAC).
// The parser must skip these 5 bytes without poisoning telbuf.
val (s, _) = makeSession()
val mccp1Start = byteArrayOf(IAC, SB, MCCP1, WILL, SE)
val text = textBytes("Hello")
val out = s.processInput(mccp1Start + text)
assertContentEquals(text, out, "MCCP1 start marker should be skipped, text should pass through")
}

@Test fun mccp1StartSequenceBetweenMsspAndText() {
// Reproduces the nanvaent bug: decompressed data contains MSSP SB + MCCP1 start,
// followed by banner text in the next chunk.
val (s, d) = makeSession()

// Chunk 1: MSSP subneg + MCCP1 start marker (no IAC SE terminator for MCCP1)
val MV: Byte = 1; val ML: Byte = 2
val chunk1 = byteArrayOf(IAC, SB, MSSP, MV) + textBytes("NAME") +
byteArrayOf(ML) + textBytes("Nanvaent") +
byteArrayOf(IAC, SE) +
byteArrayOf(IAC, SB, MCCP1, WILL, SE) // MCCP1 start (non-standard terminator)
val out1 = s.processInput(chunk1)
assertTrue(out1.isEmpty(), "Chunk 1 should contain only telnet commands")
assertEquals(1, d.msspData.size)
assertEquals("Nanvaent", d.msspData[0]["NAME"])

// Chunk 2: plain text banner
val banner = textBytes("Enter your name: ")
val out2 = s.processInput(banner)
assertContentEquals(banner, out2, "Banner text should not be swallowed by incomplete MCCP1 SB")
}

// -- Nanvaent Replay --

@Test fun nanvaentNegotiationAndMCCP2() {
val (s, d) = makeSession()
s.terminalType = "Wammer"

// Round 1: DO TTYPE
s.processInput(bytes(0xFF, 0xFD, 0x18))
assertTrue(d.allWrittenBytes.containsSequence(byteArrayOf(IAC, WILL, TTYPE)))

// Round 2: DO NAWS, WILL MCCP2, DO MXP(91), WILL MSSP, WILL 93, DO NEW_ENVIRON
d.writtenChunks.clear()
s.processInput(bytes(
0xFF, 0xFD, 0x1F, 0xFF, 0xFB, 0x56, 0xFF, 0xFD, 0x5B, 0xFF, 0xFB, 0x46, 0xFF, 0xFB, 0x5D, 0xFF,
0xFD, 0x27
))
val r2 = d.allWrittenBytes
assertTrue(r2.containsSequence(byteArrayOf(IAC, WILL, NAWS)))
assertTrue(r2.containsSequence(byteArrayOf(IAC, DO, MCCP2)))
assertTrue(r2.containsSequence(byteArrayOf(IAC, WONT, 91.toByte()))) // MXP
assertTrue(r2.containsSequence(byteArrayOf(IAC, DO, MSSP)))
assertTrue(r2.containsSequence(byteArrayOf(IAC, DONT, 93.toByte()))) // unknown
assertTrue(r2.containsSequence(byteArrayOf(IAC, WONT, 39))) // NEW_ENVIRON

// Round 3: SB TTYPE SEND
d.writtenChunks.clear()
s.processInput(bytes(0xFF, 0xFA, 0x18, 0x01, 0xFF, 0xF0))
val ttypeResponse = byteArrayOf(IAC, SB, TTYPE, ENV_IS) + textBytes("Wammer") + byteArrayOf(IAC, SE)
assertContentEquals(ttypeResponse, d.allWrittenBytes)

// Round 4: SB MCCP2 (compression starts)
d.writtenChunks.clear()
s.processInput(bytes(0xFF, 0xFA, 0x56, 0xFF, 0xF0))
assertTrue(s.isMCCP2Active)

// Round 5: compressed telnet data (MSSP + MCCP1 start)
val out5 = s.processInput(bytes(
0x78, 0xDA, 0xFA, 0xFF, 0xCB, 0x0D, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x62, 0xF4, 0x73, 0xF4, 0x75,
0x65, 0xF2, 0x4B, 0xCC, 0x2B, 0x4B, 0x4C, 0xCD, 0x2B, 0x01, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x62,
0x0C, 0xF0, 0x71, 0x8C, 0x74, 0x0D, 0x0A, 0x66, 0xB2, 0x04, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x62,
0x0C, 0x0D, 0x08, 0xF1, 0x04, 0xCA, 0x18, 0x9A, 0x9B, 0x1B, 0x5A, 0x98, 0x5B, 0x9A, 0x1A, 0x1A,
0x01, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFA, 0xFF, 0x01, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFA, 0xFF,
0x2B, 0xF4, 0xF7, 0x07, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF
))
assertEquals(1, d.msspData.size, "MSSP data should be received from compressed stream")
assertEquals("Nanvaent", d.msspData[0]["NAME"])
assertTrue(out5.isEmpty(), "Round 5 should contain only telnet commands")

// Round 6: compressed banner text
val out6 = s.processInput(bytes(
0xB4, 0x53, 0x3D, 0x6F, 0xC3, 0x20, 0x10, 0xDD, 0x2D, 0xF9, 0x3F, 0x9C, 0xB2, 0xA4, 0x1D, 0x12,
0xF6, 0x48, 0x91, 0xDA, 0xC1, 0x5B, 0x87, 0x48, 0x55, 0x87, 0xAA, 0x54, 0x16, 0x49, 0x88, 0x4D,
0x6B, 0x43, 0x64, 0x83, 0x2A, 0xFF, 0xFB, 0xDE, 0x61, 0x9C, 0x92, 0xC4, 0xAD, 0xBB, 0xE4, 0x84,
0x6C, 0xEE, 0xE3, 0x3D, 0xB8, 0x07, 0xA4, 0x09, 0xBC, 0xAD, 0xD7, 0xEB, 0xC5, 0x99, 0x61, 0xE0,
0x1D, 0xB2, 0x45, 0x2D, 0x54, 0x05, 0x2B, 0xD0, 0x81, 0xED, 0x61, 0x98, 0x2C, 0x4D, 0x53, 0xFC,
0x82, 0x4A, 0x13, 0x24, 0x9C, 0x32, 0xC6, 0xFD, 0x6F, 0xBA, 0x92, 0x01, 0xF0, 0xEB, 0x68, 0x9E,
0x4F, 0x22, 0x19, 0xAD, 0xC1, 0xC7, 0xE8, 0xA6, 0xA0, 0xAC, 0x5F, 0xF4, 0x12, 0x8B, 0x3E, 0x9B,
0x80, 0x7A, 0xE4, 0x08, 0x96, 0xF5, 0xC8, 0x3C, 0x32, 0xEF, 0x84, 0x10, 0x16, 0xE4, 0xAC, 0x77,
0x08, 0x1A, 0x65, 0x10, 0x78, 0x05, 0xF2, 0x86, 0x74, 0x77, 0x71, 0x62, 0x58, 0x97, 0xE6, 0xBC,
0x77, 0x42, 0x13, 0xA7, 0x50, 0x0E, 0x43, 0x86, 0xE8, 0x91, 0x2A, 0xA2, 0xBE, 0x0F, 0x8D, 0x9D,
0x38, 0xB9, 0xEF, 0x77, 0x80, 0x0D, 0x83, 0xE8, 0x4E, 0x21, 0xDA, 0xDE, 0x90, 0xF1, 0x9F, 0x0B,
0xAE, 0x1F, 0x3E, 0x4E, 0x83, 0xF1, 0xBE, 0xA3, 0xC8, 0x89, 0x42, 0xD4, 0x6A, 0x9F, 0xA1, 0xFD,
0xB1, 0x4B, 0x9E, 0x71, 0x9D, 0xE3, 0xB3, 0xE1, 0x91, 0xCE, 0xFF, 0xC3, 0x04, 0x54, 0xC0, 0xBC,
0xB4, 0x12, 0xE6, 0x85, 0x93, 0xAD, 0x9D, 0x83, 0x3A, 0xFC, 0x71, 0xF7, 0xBC, 0x04, 0xCC, 0xDF,
0xDC, 0xCE, 0x38, 0xF8, 0x70, 0xAD, 0x85, 0x2F, 0xA1, 0x2D, 0x58, 0x13, 0xC4, 0x65, 0x30, 0x02,
0x22, 0x32, 0xC2, 0x54, 0xC6, 0x7C, 0x82, 0x68, 0x8C, 0xD3, 0xFB, 0xE5, 0xC4, 0x0D, 0xF6, 0xA7,
0x15, 0x5E, 0x93, 0x95, 0x95, 0x96, 0x16, 0xCE, 0x9E, 0xDF, 0xA4, 0x95, 0xD6, 0x1E, 0x57, 0x8C,
0xC5, 0x20, 0x76, 0x83, 0xC7, 0xFE, 0x6A, 0x5C, 0x03, 0x8D, 0x2A, 0x4A, 0xDB, 0x42, 0x2D, 0x3A,
0xD8, 0x4A, 0xD8, 0xAB, 0x76, 0x57, 0x09, 0x55, 0x4B, 0x6C, 0x72, 0x53, 0x49, 0x81, 0xE2, 0xDA,
0xEE, 0x28, 0x61, 0x56, 0xCA, 0xEA, 0x18, 0x6A, 0x67, 0x20, 0x0E, 0x56, 0x36, 0xA8, 0x48, 0xA1,
0xF4, 0x32, 0x4D, 0x36, 0x4F, 0xD9, 0xE3, 0x73, 0x06, 0x8E, 0x6A, 0x4B, 0x3C, 0x8C, 0xAD, 0x2B,
0xE6, 0xB0, 0x33, 0x75, 0x2D, 0xF4, 0x9E, 0xC4, 0xC5, 0x3A, 0xC0, 0x58, 0xEB, 0x65, 0x3F, 0x28,
0xD4, 0x2F, 0x4D, 0x32, 0x4D, 0x0C, 0x1D, 0x6D, 0x40, 0x8B, 0x5A, 0xAE, 0xE0, 0x1B, 0x00, 0x00,
0xFF, 0xFF
))
val bannerText = String(out6, Charsets.UTF_8)
assertTrue(out6.isNotEmpty(), "Banner should not be empty")
assertTrue(bannerText.contains("Enter your name:"), "Banner should contain login prompt")
assertTrue(bannerText.contains("nanvaent.org"), "Banner should contain server name")
}
}
Loading