package session // Telnet protocol constants. // These are the IAC (Interpret As Command) sequences used to negotiate // terminal options between server and client. In the original TAG-BBS, // none of this existed — the serial port was a raw byte stream. Telnet // adds a signaling layer on top of TCP that we need to handle. const ( iacIAC byte = 255 // Interpret As Command — escapes the byte that follows iacDONT byte = 254 // Demand the client stop using an option iacDO byte = 253 // Request the client start using an option iacWONT byte = 252 // Refuse to use an option iacWILL byte = 251 // Offer to use an option iacSB byte = 250 // Subnegotiation Begin iacSE byte = 240 // Subnegotiation End iacNOP byte = 241 // No Operation iacGA byte = 249 // Go Ahead // Telnet options we care about optEcho byte = 1 // Server controls echo optSGA byte = 3 // Suppress Go-Ahead (character-at-a-time mode) optNAWS byte = 31 // Negotiate About Window Size optTTYPE byte = 24 // Terminal Type optLINEMODE byte = 34 // Linemode ) // telnetNegotiation returns the IAC sequence to send at connection start. // This puts the client into character-at-a-time mode with server-side echo, // which is what a BBS needs for single-keypress menus. // // We send: // - WILL ECHO: "I (server) will handle echoing characters back to you" // - WILL SGA: "I won't send Go-Ahead signals" (enables character mode) // - DO NAWS: "Please tell me your terminal dimensions" // - DONT LINEMODE: "Don't use linemode" (reinforces character-at-a-time) func telnetNegotiation() []byte { return []byte{ iacIAC, iacWILL, optEcho, iacIAC, iacWILL, optSGA, iacIAC, iacDO, optNAWS, iacIAC, iacDONT, optLINEMODE, } } // telnetState tracks the IAC parser state for stripping telnet commands // from the data stream. Without this, IAC sequences show up as garbage // characters in user input. type telnetState int const ( tsData telnetState = iota // Normal data tsIAC // Got IAC, next byte is command tsWill // Got WILL, next byte is option tsWont // Got WONT, next byte is option tsDo // Got DO, next byte is option tsDont // Got DONT, next byte is option tsSB // Inside subnegotiation tsSBIAC // Got IAC inside subnegotiation ) // telnetFilter strips IAC sequences from raw telnet data and returns // only the clean user input bytes. It also captures window size if the // client sends NAWS subnegotiation. // // The filter is stateful — it tracks where it is in the IAC parse across // calls, since IAC sequences can be split across TCP reads. type telnetFilter struct { state telnetState sbBuffer []byte // Accumulates subnegotiation data sbOption byte // Which option the subnegotiation is for Width int // Terminal width from NAWS (0 = unknown) Height int // Terminal height from NAWS (0 = unknown) } func newTelnetFilter() *telnetFilter { return &telnetFilter{ state: tsData, } } // Filter processes raw bytes from the TCP connection and returns only // the clean data bytes (user input). Telnet commands are consumed silently. func (f *telnetFilter) Filter(raw []byte) []byte { clean := make([]byte, 0, len(raw)) for _, b := range raw { switch f.state { case tsData: if b == iacIAC { f.state = tsIAC } else { clean = append(clean, b) } case tsIAC: switch b { case iacIAC: // Escaped 0xFF — literal data byte clean = append(clean, 0xFF) f.state = tsData case iacWILL: f.state = tsWill case iacWONT: f.state = tsWont case iacDO: f.state = tsDo case iacDONT: f.state = tsDont case iacSB: f.state = tsSB f.sbBuffer = f.sbBuffer[:0] case iacNOP, iacGA: f.state = tsData default: // Unknown command, skip f.state = tsData } case tsWill, tsWont, tsDo, tsDont: // Option byte — consumed, back to data f.state = tsData case tsSB: if b == iacIAC { f.state = tsSBIAC } else { if len(f.sbBuffer) == 0 { f.sbOption = b } f.sbBuffer = append(f.sbBuffer, b) } case tsSBIAC: if b == iacSE { // Subnegotiation complete f.handleSubnegotiation() f.state = tsData } else { // Escaped IAC within subnegotiation f.sbBuffer = append(f.sbBuffer, b) f.state = tsSB } } } return clean } // handleSubnegotiation processes a completed subnegotiation sequence. func (f *telnetFilter) handleSubnegotiation() { if len(f.sbBuffer) < 1 { return } switch f.sbOption { case optNAWS: // NAWS: option(1) + width(2) + height(2) = 5 bytes if len(f.sbBuffer) >= 5 { f.Width = int(f.sbBuffer[1])<<8 | int(f.sbBuffer[2]) f.Height = int(f.sbBuffer[3])<<8 | int(f.sbBuffer[4]) } } }