Livt packages

Livt.Net

Version 1.4.1

Ethernet, IPv4, TCP, UDP, ARP, TFTP, and Telnet networking components for Livt hardware.

Published files (163)

src/tcp/TelnetSession.lvt

namespace Livt.Net

/**
 * Bounded Telnet NVT codec with optional application echo and suppress-go-ahead. Serialize calls.
 * Drain protocol output and decoded input before feeding again. Subnegotiation is skipped
 * without storage; input CR-LF/CR-NUL becomes CR. Output accepts ASCII with CR-LF line endings;
 * IAC is escaped automatically.
 */
component TelnetSession
{
	// Telnet command bytes, introduced by IAC.
	const IAC: byte = 255
	const WILL: byte = 251
	const WONT: byte = 252
	const DO: byte = 253
	const DONT: byte = 254
	const SUBNEGOTIATION: byte = 250
	const END_SUBNEGOTIATION: byte = 240
	const ERASE_CHARACTER: byte = 247

	// Terminal control characters normalized at the stream boundary.
	const CARRIAGE_RETURN: byte = 13
	const LINE_FEED: byte = 10
	const BACKSPACE: byte = 8

	// Supported Telnet options. Echo is performed by the consuming application.
	const ECHO: byte = 1
	const SUPPRESS_GO_AHEAD: byte = 3

	// Incremental decoder states; subnegotiation requires no payload storage.
	const STATE_DATA: byte = 0
	const STATE_COMMAND: byte = 1
	const STATE_OPTION: byte = 2
	const STATE_SUBNEGOTIATION_DATA: byte = 3
	const STATE_SUBNEGOTIATION_COMMAND: byte = 4

	// Packed negotiation greeting and escaped IAC, most significant byte first.
	const GREETING: logic[48] = 0xFFFB03000000
	const ECHO_GREETING: logic[48] = 0xFFFB01FFFB03
	const ESCAPED_IAC: logic[48] = 0xFFFF00000000

	// Bit width of an encoded wire byte.
	const BYTE_BITS: int = 8

	// Incremental wire decoding and CR normalization state.
	stateCode: byte
	verb: byte
	crSeen: bool

	// One decoded input byte awaiting consumption.
	inputReady: bool
	inputByte: byte

	// Up to six encoded bytes, most significant byte first.
	output: logic[48]
	outputCount: logic[3]

	// Borrowed echo capability and pending initial offer.
	allowEcho: bool
	echoOffered: bool

	// Negotiated echo state; the application binds its line editor to this value.
	public echoEnabled: bool

	// Local and remote suppress-go-ahead negotiation state.
	localSga: bool
	remoteSga: bool

	/**
	 * Creates an idle codec in the inherited context. Call Reset before each connection.
	 * Keep allowEcho stable; when true, the application must echo accepted input only while
	 * echoEnabled is true. The codec negotiates echo but does not generate application echoes.
	 */
	new(allowEcho: bool = false)
	{
		this.allowEcho = allowEcho
	}

	/**
	 * Discards pending input/output and negotiation state, then queues WILL SUPPRESS-GO-AHEAD for
	 * the new connection. Also offers WILL ECHO when configured; echoEnabled stays false until
	 * the peer accepts, and returns to false on refusal or reset.
	 */
	public fn Reset()
	{
		this.stateCode = STATE_DATA
		this.crSeen = false
		this.inputReady = false

		// Offer suppress-go-ahead and optionally application echo for this connection.
		this.output = GREETING
		this.outputCount = 3
		this.echoEnabled = false
		this.echoOffered = this.allowEcho

		if (this.allowEcho) {
			this.output = ECHO_GREETING
			this.outputCount = 6
		}

		this.localSga = true
		this.remoteSga = false
	}

	/**
	 * Reports whether one incoming wire byte can be accepted without overwriting decoded input or
	 * pending output.
	 */
	public fn CanAccept() bool
	{
		return !this.inputReady && this.outputCount == 0
	}

	/**
	 * Reports pending encoded bytes, including negotiation replies. Drain them before accepting more
	 * input.
	 */
	public fn HasOutput() bool
	{
		return this.outputCount != 0
	}

	/**
	 * Returns the next encoded byte without consuming it. Requires HasOutput(); retry downstream
	 * admission with the same byte.
	 */
	public fn PeekOutput() byte
	{
		return (this.output >> 40) as byte
	}

	/**
	 * Removes one encoded byte after downstream acceptance; does nothing when empty.
	 */
	public fn ConsumeOutput()
	{
		if (this.outputCount != 0) {
			this.output = this.output << BYTE_BITS
			this.outputCount--
		}
	}

	/**
	 * Queues one application byte, doubling IAC when needed. Returns false without mutation while
	 * output is pending. The caller supplies CR-LF line endings.
	 */
	public fn TryWrite(value: byte) bool
	{
		if (this.outputCount != 0) {
			return false
		}

		this.output = ((value as uint) as logic[48]) << 40
		this.outputCount = 1

		if (value == IAC) {
			this.output = ESCAPED_IAC
			this.outputCount = 2
		}

		return true
	}

	/**
	 * Consumes one decoded input byte. Returns false and assigns zero when no input is ready.
	 */
	public fn TryRead(value: out byte) bool
	{
		value = 0

		if (!this.inputReady) {
			return false
		}

		value = this.inputByte
		this.inputReady = false
		return true
	}

	/**
	 * Queues a three-byte IAC/command/option response. Requires empty output and serialized access.
	 */
	fn Reply(command: byte, option: byte)
	{
		this.output = (IAC as logic[48]) << 40 | (((command as uint) as logic[48]) << 32) | (((option as uint) as logic[48]) << 24)
		this.outputCount = 3
	}

	/**
	 * Consumes one wire byte when CanAccept() is true; otherwise returns false without mutation.
	 * Maintains negotiation state across calls and may produce decoded input or protocol output.
	 */
	public fn Accept(value: byte) bool
	{
		if (!this.CanAccept()) {
			return false
		}

		if (this.stateCode == STATE_SUBNEGOTIATION_DATA) {
			if (value == IAC) {
				this.stateCode = STATE_SUBNEGOTIATION_COMMAND
			}

			return true
		}

		if (this.stateCode == STATE_SUBNEGOTIATION_COMMAND) {
			if (value == END_SUBNEGOTIATION) {
				this.stateCode = STATE_DATA
			} else {
				this.stateCode = STATE_SUBNEGOTIATION_DATA
			}

			return true
		}

		if (this.stateCode == STATE_OPTION) {
			this.stateCode = STATE_DATA

			if (this.verb == DO) {
				if (value == ECHO && this.allowEcho) {
					if (!this.echoEnabled && !this.echoOffered) {
						this.Reply(WILL, value)
					}

					this.echoEnabled = true
					this.echoOffered = false
				} elif (value == SUPPRESS_GO_AHEAD) {
					if (!this.localSga) {
						this.Reply(WILL, value)
						this.localSga = true
					}
				} else {
					this.Reply(WONT, value)
				}
			} elif (this.verb == DONT) {
				if (value == ECHO) {
					this.echoOffered = false

					if (this.echoEnabled) {
						this.echoEnabled = false
						this.Reply(WONT, value)
					}
				} elif (value == SUPPRESS_GO_AHEAD && this.localSga) {
					this.localSga = false
					this.Reply(WONT, value)
				}
			} elif (this.verb == WILL) {
				if (value == SUPPRESS_GO_AHEAD) {
					if (!this.remoteSga) {
						this.remoteSga = true
						this.Reply(DO, value)
					}
				} else {
					this.Reply(DONT, value)
				}
			} elif (this.verb == WONT && value == SUPPRESS_GO_AHEAD && this.remoteSga) {
				this.remoteSga = false
				this.Reply(DONT, value)
			}

			return true
		}

		if (this.stateCode == STATE_COMMAND) {
			this.stateCode = STATE_DATA

			if (value >= WILL && value <= DONT) {
				this.verb = value
				this.stateCode = STATE_OPTION
				return true
			}

			if (value == SUBNEGOTIATION) {
				this.stateCode = STATE_SUBNEGOTIATION_DATA
				return true
			}

			if (value == ERASE_CHARACTER) {
				this.inputByte = BACKSPACE
				this.inputReady = true
				return true
			}

			if (value != IAC) {
				return true
			}
		} elif (value == IAC) {
			this.stateCode = STATE_COMMAND
			return true
		}

		if (this.crSeen && (value == 0 || value == LINE_FEED)) {
			this.crSeen = false
			return true
		}

		this.crSeen = value == CARRIAGE_RETURN
		this.inputByte = value
		this.inputReady = true
		return true
	}
}