Files
meow/lib/meow/telnet.rb
T
2026-09-17 01:08:16 +02:00

149 lines
4.1 KiB
Ruby

module MeowClient
module Telnet
IAC = 255
DONT = 254
DO = 253
WONT = 252
WILL = 251
SB = 250
SE = 240
ECHO = 1
SUPPRESS_GO_AHEAD = 3
TERMINAL_TYPE = 24
TTYPE_IS = 0
TTYPE_SEND = 1
Result = Struct.new(:data, :replies, :events, keyword_init: true)
class Parser
attr_reader :remote_echo
def initialize
@state = :data
@negotiation_command = nil
@subnegotiation_option = nil
@subnegotiation = +"".b
@remote_echo = false
@remote_options = {}
@local_options = {}
end
def feed(bytes)
output = +"".b
replies = []
events = []
bytes.to_s.b.each_byte do |byte|
case @state
when :data
if byte == IAC
@state = :iac
else
output << byte
end
when :iac
handle_iac(byte, output)
when :negotiation
reply, event = negotiate(@negotiation_command, byte)
replies << reply if reply != nil
events << event if event != nil
@state = :data
when :sb_option
@subnegotiation_option = byte
@subnegotiation.clear
@state = :sb
when :sb
if byte == IAC
@state = :sb_iac
else
@subnegotiation << byte
end
when :sb_iac
if byte == IAC
@subnegotiation << IAC
@state = :sb
elsif byte == SE
reply, event = finish_subnegotiation
replies << reply if reply != nil
events << event if event != nil
@state = :data
else
@state = :data
end
end
end
Result.new(:data => output, :replies => replies, :events => events)
end
private
def handle_iac(byte, output)
case byte
when IAC
output << IAC
@state = :data
when WILL, WONT, DO, DONT
@negotiation_command = byte
@state = :negotiation
when SB
@state = :sb_option
else
@state = :data
end
end
def packet(command, option)
[IAC, command, option].pack("C*")
end
def negotiate(command, option)
case command
when WILL
enabled = [ECHO, SUPPRESS_GO_AHEAD].include?(option)
changed = @remote_options[option] != enabled
@remote_options[option] = enabled
return [nil, nil] unless changed
if option == ECHO
@remote_echo = enabled
return [packet(enabled ? DO : DONT, option), [:remote_echo, enabled]]
end
[packet(enabled ? DO : DONT, option), nil]
when WONT
changed = @remote_options[option] != false
@remote_options[option] = false
if option == ECHO
@remote_echo = false
return [packet(DONT, option), [:remote_echo, false]] if changed
end
[changed ? packet(DONT, option) : nil, nil]
when DO
enabled = [SUPPRESS_GO_AHEAD, TERMINAL_TYPE].include?(option)
changed = @local_options[option] != enabled
@local_options[option] = enabled
[changed ? packet(enabled ? WILL : WONT, option) : nil, nil]
when DONT
changed = @local_options[option] != false
@local_options[option] = false
[changed ? packet(WONT, option) : nil, nil]
else
[nil, nil]
end
end
def finish_subnegotiation
if @subnegotiation_option == TERMINAL_TYPE &&
@local_options[TERMINAL_TYPE] == true &&
@subnegotiation.getbyte(0) == TTYPE_SEND
body = [IAC, SB, TERMINAL_TYPE, TTYPE_IS].pack("C*") + "MEOW".b
return [body + [IAC, SE].pack("C*"), [:terminal_type, "MEOW"]]
end
[nil, [:subnegotiation, @subnegotiation_option]]
ensure
@subnegotiation_option = nil
@subnegotiation.clear
end
end
end
end