// Package net implements the server transports: telnet, telnet-over-TLS, // and the WebSocket bridge for the web client, plus the shared Session type. // It must not be confused with the standard library net package. package net import ( "bufio" "bytes" "io" "net" "strings" "sync/atomic" "time" "unicode/utf8" ) type Conn interface { ReadMessage() (string, error) io.Writer io.Closer // SetWriteDeadline is optional for transports: sessions that support it // enforce per-write deadlines so a stalled peer surfaces as a timeout // instead of a hung writer goroutine. Implementations without deadline // support return nil (deadlines are best-effort). SetWriteDeadline(t time.Time) error // SetReadDeadline is the read-side counterpart: transports implement it so // a silent peer cannot pin a goroutine and a connection slot forever. // Implementations without deadline support return nil. SetReadDeadline(t time.Time) error SetEcho(bool) error } type tcpConn struct { conn net.Conn reader *bufio.Reader // echoOff is written by the owner goroutine (SetEcho/SetEchoState) and // read by this connection's reader goroutine, so it must be atomic. echoOff atomic.Bool } func newTCPConn(conn net.Conn) *tcpConn { return &tcpConn{ conn: conn, reader: bufio.NewReaderSize(conn, maxLineLength*4), } } // maxLineLength bounds how much of an incoming line is accepted. Longer // lines are drained from the socket and truncated so a client cannot grow // the read buffer without limit. const maxLineLength = 1024 func (t *tcpConn) ReadMessage() (string, error) { line, err := t.reader.ReadSlice('\n') if err == bufio.ErrBufferFull { // Copy the bounded prefix out of the reusable read buffer BEFORE // draining the rest of the oversized line; further reads overwrite the // buffer the slice points into. prefixLen := len(line) if prefixLen > maxLineLength { prefixLen = maxLineLength } prefix := make([]byte, prefixLen) copy(prefix, line[:prefixLen]) for { _, err = t.reader.ReadSlice('\n') if err == nil { break } if err != bufio.ErrBufferFull { return "", err } } line = prefix } else if err != nil { return "", err } text := strings.TrimRight(string(line), "\r\n") text = stripIAC(text) text = truncateLine(text, maxLineLength) if t.echoOff.Load() { // Refresh the write deadline: the writer goroutine may have left a // stale deadline in the past, which would silently drop this echo on // an idle connection. _ = t.conn.SetWriteDeadline(time.Now().Add(writeDeadline)) _, _ = t.conn.Write([]byte("\r\n")) } return text, nil } func (t *tcpConn) Write(b []byte) (int, error) { return t.conn.Write(b) } // RemoteAddr reports the peer address for the login throttle. func (t *tcpConn) RemoteAddr() string { if t.conn == nil { return "" } return t.conn.RemoteAddr().String() } // truncateLine cuts s to at most max bytes without splitting a UTF-8 rune. // Both the telnet and websocket paths bound line length, and a byte-boundary // cut would otherwise emit an invalid trailing rune to the game. func truncateLine(s string, max int) string { if max <= 0 || len(s) <= max { return s } s = s[:max] for len(s) > 0 { r, size := utf8.DecodeLastRuneInString(s) if r != utf8.RuneError || size > 1 { break } s = s[:len(s)-1] } return s } func (t *tcpConn) SetWriteDeadline(ts time.Time) error { return t.conn.SetWriteDeadline(ts) } func (t *tcpConn) SetReadDeadline(ts time.Time) error { return t.conn.SetReadDeadline(ts) } func (t *tcpConn) Close() error { return t.conn.Close() } func (t *tcpConn) SetEcho(on bool) error { t.echoOff.Store(!on) if on { return t.writeTelnetCmd(wont, echo) } return t.writeTelnetCmd(will, echo) } // EchoSequence returns the telnet negotiation bytes that switch the client's // echo state. Queued sessions transmit these through the session writer so a // stalled client cannot block the caller (see Session.SetEcho); the local // echo state is applied separately via SetEchoState. func (t *tcpConn) EchoSequence(on bool) []byte { if on { return []byte{iac, wont, echo} } return []byte{iac, will, echo} } // SetEchoState records the echo state without writing to the network; used // when the negotiation bytes are queued instead of written inline. func (t *tcpConn) SetEchoState(on bool) { t.echoOff.Store(!on) } const ( iac = 255 will = 251 wont = 252 do = 253 dont = 254 sb = 250 se = 240 echo = 1 ) func (t *tcpConn) writeTelnetCmd(cmd, opt byte) error { _, err := t.conn.Write([]byte{iac, cmd, opt}) return err } func stripIAC(s string) string { b := []byte(s) var out []byte i := 0 iacSE := []byte{iac, se} for i < len(b) { if b[i] != iac { out = append(out, b[i]) i++ continue } // IAC begins a telnet command. If the line ends mid-command, drop the // remainder: leaking the option byte into player input would corrupt // the command/password with a stray non-UTF-8 byte. if i+1 >= len(b) { break } if b[i+1] == sb { end := bytes.Index(b[i+2:], iacSE) if end < 0 { break } i += end + 4 continue } i += 3 if i > len(b) { i = len(b) } } return string(out) }