Initial Meow MUD client
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user