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
13 changes: 13 additions & 0 deletions Sources/SwiftMTHClient/TelnetClientSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public final class TelnetClientSession {
public private(set) var gmcpEnabled: Bool = false
public private(set) var msdpEnabled: Bool = false
public private(set) var msspEnabled: Bool = false
public private(set) var mxpEnabled: Bool = false

// MARK: - Private State

Expand Down Expand Up @@ -188,6 +189,10 @@ public final class TelnetClientSession {
TeloptPattern(pattern: [TC.IAC, TC.SB, TO.GMCP],
handler: { s, src, i, n in s.processSbGmcp(src, at: i, srclen: n) }),

// MXP — accept the server's offer so it may send MXP markup.
TeloptPattern(pattern: [TC.IAC, TC.WILL, TO.MXP],
handler: { s, _, _, _ in s.processWillMxp(); return 3 }),

// MSDP
TeloptPattern(pattern: [TC.IAC, TC.WILL, TO.MSDP],
handler: { s, _, _, _ in s.processWillMsdp(); return 3 }),
Expand Down Expand Up @@ -314,6 +319,14 @@ public final class TelnetClientSession {
delegate?.onGMCPNegotiated()
}

// MARK: - Handler: MXP

private func processWillMxp() {
mxpEnabled = true
serverOptions.insert(TO.MXP)
write([TC.IAC, TC.DO, TO.MXP])
}

private func processSbGmcp(_ src: [UInt8], at offset: Int, srclen: Int) -> Int {
let sbLen = skipSB(src, at: offset, srclen: srclen)
if sbLen > srclen { return srclen + 1 }
Expand Down
2 changes: 1 addition & 1 deletion Sources/cmth/mth.c
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ struct telnet_type telnet_table[] =
{ "88", 0 },
{ "89", 0 },
{ "MSP", 0 },
{ "MXP", 0 },
{ "MXP", ANNOUNCE_WILL },
{ "MSP2", 0 }, /* Unadopted */
{ "ZMP", 0 }, /* Unadopted */
{ "94", 0 },
Expand Down
1 change: 1 addition & 0 deletions Sources/cmth/mth.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ typedef struct descriptor_data DESCRIPTOR_DATA;
#define COMM_FLAG_256COLORS BV06
#define COMM_FLAG_UTF8 BV07
#define COMM_FLAG_GMCP BV08
#define COMM_FLAG_MXP BV09

#define MSDP_FLAG_COMMAND BV01
#define MSDP_FLAG_LIST BV02
Expand Down
31 changes: 31 additions & 0 deletions Sources/cmth/telopt.c
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ int process_do_msdp ( DESCRIPTOR_DATA *d, unsigned char *src, i
int process_sb_msdp ( DESCRIPTOR_DATA *d, unsigned char *src, int srclen );
int process_do_gmcp ( DESCRIPTOR_DATA *d, unsigned char *src, int srclen );
int process_sb_gmcp ( DESCRIPTOR_DATA *d, unsigned char *src, int srclen );
int process_do_mxp ( DESCRIPTOR_DATA *d, unsigned char *src, int srclen );
int process_dont_mxp ( DESCRIPTOR_DATA *d, unsigned char *src, int srclen );
int process_do_mccp2 ( DESCRIPTOR_DATA *d, unsigned char *src, int srclen );
int process_dont_mccp2 ( DESCRIPTOR_DATA *d, unsigned char *src, int srclen );
int skip_sb ( DESCRIPTOR_DATA *d, unsigned char *src, int srclen );
Expand Down Expand Up @@ -67,6 +69,9 @@ const struct telopt_type telopt_table [] =
{ 3, (unsigned char []) { IAC, DO, TELOPT_GMCP, 0 }, &process_do_gmcp},
{ 3, (unsigned char []) { IAC, SB, TELOPT_GMCP, 0 }, &process_sb_gmcp},

{ 3, (unsigned char []) { IAC, DO, TELOPT_MXP, 0 }, &process_do_mxp},
{ 3, (unsigned char []) { IAC, DONT, TELOPT_MXP, 0 }, &process_dont_mxp},

{ 3, (unsigned char []) { IAC, DO, TELOPT_MCCP2, 0 }, &process_do_mccp2},
{ 3, (unsigned char []) { IAC, DONT, TELOPT_MCCP2, 0 }, &process_dont_mccp2},

Expand Down Expand Up @@ -863,6 +868,32 @@ int process_sb_msdp( DESCRIPTOR_DATA *d, unsigned char *src, int srclen )
return i + 1;
}

// MXP

int process_do_mxp( DESCRIPTOR_DATA *d, unsigned char *src, int srclen )
{
if (HAS_BIT(d->mth->comm_flags, COMM_FLAG_MXP))
{
return 3;
}

SET_BIT(d->mth->comm_flags, COMM_FLAG_MXP);

/* ESC[7z — Lock Locked: make "locked" the persistent default line mode, so
normal output is never parsed as MXP markup. Links opt back in per-span. */
descriptor_printf(d, "\033[7z");
log_descriptor_printf(d, "INFO MXP ENABLED");

return 3;
}

int process_dont_mxp( DESCRIPTOR_DATA *d, unsigned char *src, int srclen )
{
DEL_BIT(d->mth->comm_flags, COMM_FLAG_MXP);

return 3;
}

// MSDP over GMCP

int process_do_gmcp( DESCRIPTOR_DATA *d, unsigned char *src, int srclen )
Expand Down
8 changes: 8 additions & 0 deletions Tests/MTHTests/TelnetClientSessionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ struct TelnetClientSessionTests {
#expect(out == input)
}

@Test func mxpWillIsAcceptedWithDo() {
let (s, d) = makeSession()
#expect(!s.mxpEnabled)
_ = s.processInput([TC.IAC, TC.WILL, TO.MXP])
#expect(s.mxpEnabled)
#expect(d.allWrittenBytes.containsSequence([TC.IAC, TC.DO, TO.MXP]))
}

@Test func emptyInput() {
let (s, _) = makeSession()
let out = s.processInput([])
Expand Down
2 changes: 1 addition & 1 deletion kotlin/mth-core/src/main/kotlin/mth/core/AnnounceFlags.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@ val defaultTelnetTable: List<TelnetOptionEntry> = buildList {
this[86] = TelnetOptionEntry("MCCP2", AnnounceFlags.WILL)
this[87] = TelnetOptionEntry("MCCP3", AnnounceFlags.WILL)
this[90] = TelnetOptionEntry("MSP", AnnounceFlags.WILL)
this[91] = TelnetOptionEntry("MXP")
this[91] = TelnetOptionEntry("MXP", AnnounceFlags.WILL)
this[201] = TelnetOptionEntry("GMCP", AnnounceFlags.WILL)
}
1 change: 1 addition & 0 deletions kotlin/mth-core/src/main/kotlin/mth/core/CommFlags.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ value class CommFlags(val rawValue: Int = 0) {
val COLORS_256 = CommFlags(1 shl 5)
val UTF8 = CommFlags(1 shl 6)
val GMCP = CommFlags(1 shl 7)
val MXP = CommFlags(1 shl 8)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ class TelnetClientSession(
var msspEnabled: Boolean = false
private set

/** Whether MXP has been negotiated. */
var mxpEnabled: Boolean = false
private set

// -- Private State --

/** Buffer for incomplete telnet sequences (packet fragmentation). */
Expand Down Expand Up @@ -230,6 +234,10 @@ class TelnetClientSession(
TeloptPattern(byteArrayOf(TC.IAC, TC.SB, TO.GMCP))
{ s, src, i, n -> s.processSbGmcp(src, i, n) },

// Server offers MXP — accept so it may send MXP markup.
TeloptPattern(byteArrayOf(TC.IAC, TC.WILL, TO.MXP))
{ s, _, _, _ -> s.processWillMxp(); 3 },

// Server offers MCCP2
TeloptPattern(byteArrayOf(TC.IAC, TC.WILL, TO.MCCP2))
{ s, _, _, _ -> s.processWillMccp2(); 3 },
Expand Down Expand Up @@ -346,6 +354,12 @@ class TelnetClientSession(
delegate?.onGMCPNegotiated()
}

private fun processWillMxp() {
mxpEnabled = true
serverOptions.add(TO.MXP)
write(byteArrayOf(TC.IAC, TC.DO, TO.MXP))
}

private fun processSbGmcp(src: ByteArray, offset: Int, srclen: Int): Int {
val sbLen = skipSB(src, offset, srclen)
if (sbLen > srclen) return srclen + 1
Expand Down
32 changes: 32 additions & 0 deletions kotlin/mth-core/src/main/kotlin/mth/core/server/TelnetSession.kt
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,14 @@ class TelnetSession(
write(byteArrayOf(TC.IAC, TC.WILL, TO.GMCP))
}

/** Whether the client negotiated MXP (telnet option 91). */
val mxpEnabled: Boolean get() = CommFlags.MXP in commFlags

/** Re-assert the locked-default MXP line mode after a copyover restore. */
fun reassertMXP() {
if (CommFlags.MXP in commFlags) write(MXP_LOCKED_DEFAULT)
}

/** Send echo-off (password mode). */
fun sendEchoOff() {
commFlags = commFlags.insert(CommFlags.PASSWORD)
Expand Down Expand Up @@ -342,6 +350,11 @@ class TelnetSession(
TeloptPattern(byteArrayOf(TC.IAC, TC.SB, TO.GMCP))
{ s, src, i, n -> s.processSbGmcp(src, i, n) },

TeloptPattern(byteArrayOf(TC.IAC, TC.DO, TO.MXP))
{ s, _, _, _ -> s.processDoMxp(); 3 },
TeloptPattern(byteArrayOf(TC.IAC, TC.DONT, TO.MXP))
{ s, _, _, _ -> s.processDontMxp(); 3 },

// MCCP2
TeloptPattern(byteArrayOf(TC.IAC, TC.DO, TO.MCCP2))
{ s, _, _, _ -> s.processDoMccp2(); 3 },
Expand Down Expand Up @@ -728,6 +741,19 @@ class TelnetSession(
return sbLen
}

// -- Handler: MXP --

private fun processDoMxp() {
if (CommFlags.MXP in commFlags) return
commFlags = commFlags.insert(CommFlags.MXP)
write(MXP_LOCKED_DEFAULT)
log("INFO MXP ENABLED")
}

private fun processDontMxp() {
commFlags = commFlags.remove(CommFlags.MXP)
}

// -- Handler: GMCP --

private fun processDoGmcp() {
Expand Down Expand Up @@ -857,4 +883,10 @@ class TelnetSession(
log("MCCP3: COMPRESSION END")
mccp3 = null
}

private companion object {
// ESC[7z — Lock Locked: makes "locked" the persistent default line mode so normal
// output is never parsed as MXP markup; links opt back in per-span with ESC[1z…ESC[2z.
val MXP_LOCKED_DEFAULT = byteArrayOf(0x1B, 0x5B, 0x37, 0x7A)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,14 @@ class TelnetClientSessionTest {
assertContentEquals(byteArrayOf(IAC, DO, GMCP), d.allWrittenBytes)
}

@Test fun serverWillMxpRespondsDoMxp() {
val (s, d) = makeSession()
assertFalse(s.mxpEnabled)
s.processInput(bytes(0xFF, 0xFB, 91))
assertTrue(s.mxpEnabled)
assertContentEquals(bytes(0xFF, 0xFD, 91), d.allWrittenBytes)
}

@Test fun serverSendsGmcpDataParsed() {
val (s, d) = makeSession()
s.processInput(byteArrayOf(IAC, WILL, GMCP))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,30 @@ class TelnetSessionTest {
assertTrue(out.isEmpty())
}

// -- MXP --

@Test fun announcesWillMxp() {
val (s, d) = makeSession()
s.announceSupport()
assertTrue(d.allWrittenBytes.containsSequence(bytes(0xFF, 0xFB, 91)))
}

@Test fun doMxpEnablesAndLocksDefault() {
val (s, d) = makeSession()
assertFalse(s.mxpEnabled)
s.processInput(bytes(0xFF, 0xFD, 91))
assertTrue(s.mxpEnabled)
assertTrue(d.allWrittenBytes.containsSequence(bytes(0x1B, 0x5B, 0x37, 0x7A)))
}

@Test fun dontMxpDisables() {
val (s, _) = makeSession()
s.processInput(bytes(0xFF, 0xFD, 91))
assertTrue(s.mxpEnabled)
s.processInput(bytes(0xFF, 0xFE, 91))
assertFalse(s.mxpEnabled)
}

// -- CR/NUL Handling --

@Test fun crNulConvertsToNewline() {
Expand Down
Loading