149 lines
4.1 KiB
Ruby
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
|
|
|