|
@@ -1,43 +1,32 @@
|
|
|
|
+require 'zlib'
|
|
|
|
+require_relative 'telnet/codes'
|
|
|
|
+require_relative 'rfc1143'
|
|
|
|
+require_relative 'monolog'
|
|
|
|
|
|
-# file:: telnetfilter.rb
|
|
|
|
-# author:: Jon A. Lambert
|
|
|
|
-# version:: 2.8.0
|
|
|
|
-# date:: 01/19/2006
|
|
|
|
-#
|
|
|
|
-# This source code copyright (C) 2005, 2006 by Jon A. Lambert
|
|
|
|
-# All rights reserved.
|
|
|
|
-#
|
|
|
|
-# Released under the terms of the TeensyMUD Public License
|
|
|
|
-# See LICENSE file for additional information.
|
|
|
|
-#
|
|
|
|
-$:.unshift "lib" if !$:.include? "lib"
|
|
|
|
-$:.unshift "vendor" if !$:.include? "vendor"
|
|
|
|
|
|
|
|
-require 'strscan'
|
|
|
|
-require 'ostruct'
|
|
|
|
-require 'network/protocol/filter'
|
|
|
|
-require 'network/protocol/telnetcodes'
|
|
|
|
-require 'network/protocol/asciicodes'
|
|
|
|
|
|
|
|
-# The TelnetFilter class implements the Telnet protocol.
|
|
|
|
-#
|
|
|
|
-# This implements most of basic Telnet as per RFCs 854/855/1129/1143 and
|
|
|
|
-# options in RFCs 857/858/1073/1091
|
|
|
|
|
|
+# This Telnet class implements a subset of the Telnet protocol.
|
|
#
|
|
#
|
|
class Telnet
|
|
class Telnet
|
|
|
|
+ include Monolog
|
|
include Telnet::Codes
|
|
include Telnet::Codes
|
|
|
|
|
|
- # Initialize state of filter
|
|
|
|
- def initialize(server)
|
|
|
|
- @server = server
|
|
|
|
- @mode = :normal # Parse mode :normal, :cmd, :cr
|
|
|
|
- @state = {}
|
|
|
|
- @sc = nil
|
|
|
|
- @sneg_opts = [ TTYPE, ZMP ]
|
|
|
|
- @ttype = []
|
|
|
|
- @init_tries = 0 # Number of tries at negotitating sub options
|
|
|
|
- @synch = false
|
|
|
|
- log.debug "telnet filter initialized - #{@init_tries}"
|
|
|
|
|
|
+ # Allowed telnet state codes
|
|
|
|
+ STATES = [:data, :iac, :will, :wont, :do, :dont, :sb, :sb_data, :sb_data_iac]
|
|
|
|
+
|
|
|
|
+ # Helper structs
|
|
|
|
+ Telopt = Struct.new(:telopt, :us, :him)
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ def initialize(client)
|
|
|
|
+ @client = client
|
|
|
|
+ @telopts = {} # Telopt support.
|
|
|
|
+ @rfc1143 = {} # RFC1143 support.
|
|
|
|
+ @buffer = "" # Subrequest buffer
|
|
|
|
+ @state = :data # state of telnet protocol parser.
|
|
|
|
+ @sb_telopt = nil; # current subnegotiation
|
|
|
|
+ @compress = false # compression state
|
|
end
|
|
end
|
|
|
|
|
|
# Wait for input from the server
|
|
# Wait for input from the server
|
|
@@ -47,693 +36,130 @@ class Telnet
|
|
|
|
|
|
# Called when client data should be filtered before being passed to the server
|
|
# Called when client data should be filtered before being passed to the server
|
|
def client_to_server(data)
|
|
def client_to_server(data)
|
|
-
|
|
|
|
- end
|
|
|
|
-
|
|
|
|
- # Called when server data should be filtered before being passed to the client
|
|
|
|
- def server_to_client(data)
|
|
|
|
-
|
|
|
|
- end
|
|
|
|
-
|
|
|
|
-
|
|
|
|
-
|
|
|
|
-
|
|
|
|
- # Negotiate starting wanted options
|
|
|
|
- #
|
|
|
|
- # [+args+] Optional initial options
|
|
|
|
- def init(args)
|
|
|
|
- if @server.service_type == :client # let server offer and ask for client
|
|
|
|
- # several sorts of options here - server offer, ask client or both
|
|
|
|
- @wopts.each do |key,val|
|
|
|
|
- case key
|
|
|
|
- when ECHO, SGA, BINARY, ZMP, EOREC
|
|
|
|
- ask_him(key,val)
|
|
|
|
- else
|
|
|
|
- offer_us(key,val)
|
|
|
|
- end
|
|
|
|
- end
|
|
|
|
- else
|
|
|
|
- # several sorts of options here - server offer, ask client or both
|
|
|
|
- @wopts.each do |key,val|
|
|
|
|
- case key
|
|
|
|
- when ECHO, SGA, BINARY, ZMP, EOREC
|
|
|
|
- offer_us(key,val)
|
|
|
|
- else
|
|
|
|
- ask_him(key,val)
|
|
|
|
- end
|
|
|
|
- end
|
|
|
|
- end
|
|
|
|
- true
|
|
|
|
- end
|
|
|
|
-
|
|
|
|
- # The filter_in method filters input data
|
|
|
|
- # [+str+] The string to be processed
|
|
|
|
- # [+return+] The filtered data
|
|
|
|
- def filter_in(str)
|
|
|
|
-# init_subneg
|
|
|
|
- return "" if str.nil? || str.empty?
|
|
|
|
- buf = ""
|
|
|
|
-
|
|
|
|
- @sc ? @sc.concat(str) : @sc = StringScanner.new(str)
|
|
|
|
- while b = @sc.get_byte
|
|
|
|
-
|
|
|
|
- # OOB sync data
|
|
|
|
- if @pstack.urgent_on || b.getbyte(0) == DM
|
|
|
|
- log.debug("(#{@pstack.conn.object_id}) Sync mode on")
|
|
|
|
- @pstack.urgent_on = false
|
|
|
|
- @synch = true
|
|
|
|
- break
|
|
|
|
- end
|
|
|
|
-
|
|
|
|
- case mode?
|
|
|
|
- when :normal
|
|
|
|
- case b.getbyte(0)
|
|
|
|
- when CR
|
|
|
|
- next if @synch
|
|
|
|
- set_mode(:cr) if !@pstack.binary_on
|
|
|
|
- when LF # LF or LF/CR may be issued by broken mud servers and clients
|
|
|
|
- next if @synch
|
|
|
|
- set_mode(:lf) if !@pstack.binary_on
|
|
|
|
- buf << LF.chr
|
|
|
|
- echo(CR.chr + LF.chr)
|
|
|
|
- when IAC
|
|
|
|
- set_mode(:cmd)
|
|
|
|
- when NUL # ignore NULs in stream when in normal mode
|
|
|
|
- next if @synch
|
|
|
|
- if @pstack.binary_on
|
|
|
|
- buf << b
|
|
|
|
- echo(b)
|
|
|
|
- else
|
|
|
|
- log.debug("(#{@pstack.conn.object_id}) unexpected NUL found in stream")
|
|
|
|
- end
|
|
|
|
- when BS, DEL
|
|
|
|
- next if @synch
|
|
|
|
- # Leaves BS, DEL in input stream for higher filter to deal with.
|
|
|
|
- buf << b
|
|
|
|
- echo(BS.chr)
|
|
|
|
|
|
+ result = ""
|
|
|
|
+ data.each_byte do | b |
|
|
|
|
+ iac = TELNET_IAC.chr
|
|
|
|
+
|
|
|
|
+ case @buffer
|
|
|
|
+ when /\A#{iac}#{iac}\Z/
|
|
|
|
+
|
|
|
|
+ # ongoing negotiation
|
|
|
|
+ when /\A#{iac}\Z/
|
|
|
|
+ return nil
|
|
else
|
|
else
|
|
- next if @synch
|
|
|
|
- ### NOTE - we will allow 8-bit NVT against RFC 1123 recommendation "should not"
|
|
|
|
- ###
|
|
|
|
- # Only let 7-bit values through in normal mode
|
|
|
|
- #if (b[0] & 0x80 == 0) && !@pstack.binary_on
|
|
|
|
- buf << b
|
|
|
|
- echo(b)
|
|
|
|
- #else
|
|
|
|
- # log.debug("(#{@pstack.conn.object_id}) unexpected 8-bit byte found in stream '#{b[0]}'")
|
|
|
|
- #end
|
|
|
|
- end
|
|
|
|
- when :cr
|
|
|
|
- # handle CRLF and CRNUL by insertion of LF into buffer
|
|
|
|
- case b.getbyte(0)
|
|
|
|
- when LF
|
|
|
|
- buf << LF.chr
|
|
|
|
- echo(CR.chr + LF.chr)
|
|
|
|
- when NUL
|
|
|
|
- if @server.service_type == :client # Don't xlate CRNUL when client
|
|
|
|
- buf << CR.chr
|
|
|
|
- echo(CR.chr)
|
|
|
|
- else
|
|
|
|
- buf << LF.chr
|
|
|
|
- echo(CR.chr + LF.chr)
|
|
|
|
- end
|
|
|
|
- else # eat lone CR
|
|
|
|
- buf << b
|
|
|
|
- echo(b)
|
|
|
|
- end
|
|
|
|
- set_mode(:normal)
|
|
|
|
- when :lf
|
|
|
|
- # liberally handle LF, LFCR for clients that aren't telnet correct
|
|
|
|
- case b.getbyte(0)
|
|
|
|
- when CR # Handle LFCR by swallowing CR
|
|
|
|
- else # Handle other stuff that follows - single LF
|
|
|
|
- buf << b
|
|
|
|
- echo(b)
|
|
|
|
- end
|
|
|
|
- set_mode(:normal)
|
|
|
|
- when :cmd
|
|
|
|
- case b.getbyte(0)
|
|
|
|
- when IAC
|
|
|
|
- # IAC escapes IAC
|
|
|
|
- buf << IAC.chr
|
|
|
|
- set_mode(:normal)
|
|
|
|
- when AYT
|
|
|
|
- log.debug("(#{@pstack.conn.object_id}) AYT sent - Msg returned")
|
|
|
|
- @pstack.conn.sock.send("WOE is here.\n",0)
|
|
|
|
- set_mode(:normal)
|
|
|
|
- when AO
|
|
|
|
- log.debug("(#{@pstack.conn.object_id}) AO sent - Synch returned")
|
|
|
|
- @pstack.conn.sockio.write_flush
|
|
|
|
- @pstack.conn.sock.send(IAC.chr + DM.chr, 0)
|
|
|
|
- @pstack.conn.sockio.write_urgent(DM.chr)
|
|
|
|
- set_mode(:normal)
|
|
|
|
- when IP
|
|
|
|
- @pstack.conn.sockio.read_flush
|
|
|
|
- @pstack.conn.sockio.write_flush
|
|
|
|
- log.debug("(#{@pstack.conn.object_id}) IP sent")
|
|
|
|
- set_mode(:normal)
|
|
|
|
- when GA, NOP, BRK # not implemented or ignored
|
|
|
|
- log.debug("(#{@pstack.conn.object_id}) GA, NOP or BRK sent")
|
|
|
|
- set_mode(:normal)
|
|
|
|
- when DM
|
|
|
|
- log.debug("(#{@pstack.conn.object_id}) Synch mode off")
|
|
|
|
- @synch = false
|
|
|
|
- set_mode(:normal)
|
|
|
|
- when EC
|
|
|
|
- next if @synch
|
|
|
|
- log.debug("(#{@pstack.conn.object_id}) EC sent")
|
|
|
|
- if buf.size > 1
|
|
|
|
- buf.slice!(-1)
|
|
|
|
- elsif @pstack.conn.inbuffer.size > 0
|
|
|
|
- @pstack.conn.inbuffer.slice(-1)
|
|
|
|
- end
|
|
|
|
- set_mode(:normal)
|
|
|
|
- when EL
|
|
|
|
- next if @synch
|
|
|
|
- log.debug("(#{@pstack.conn.object_id}) EL sent")
|
|
|
|
- p = buf.rindex("\n")
|
|
|
|
- if p
|
|
|
|
- buf.slice!(p+1..-1)
|
|
|
|
- else
|
|
|
|
- buf = ""
|
|
|
|
- p = @pstack.conn.inbuffer.rindex("\n")
|
|
|
|
- if p
|
|
|
|
- @pstack.conn.inbuffer.slice!(p+1..-1)
|
|
|
|
- end
|
|
|
|
- end
|
|
|
|
- set_mode(:normal)
|
|
|
|
- when DO, DONT, WILL, WONT
|
|
|
|
- if @sc.eos?
|
|
|
|
- @sc.unscan
|
|
|
|
- break
|
|
|
|
- end
|
|
|
|
- opt = @sc.get_byte
|
|
|
|
- case b.getbyte(0)
|
|
|
|
- when WILL
|
|
|
|
- replies_him(opt.getbyte(0),true)
|
|
|
|
- when WONT
|
|
|
|
- replies_him(opt.getbyte(0),false)
|
|
|
|
- when DO
|
|
|
|
- requests_us(opt.getbyte(0),true)
|
|
|
|
- when DONT
|
|
|
|
- requests_us(opt.getbyte(0),false)
|
|
|
|
- end
|
|
|
|
- # Update interesting things in ProtocolStack after negotiation
|
|
|
|
- case opt.getbyte(0)
|
|
|
|
- when ECHO
|
|
|
|
- @pstack.echo_on = enabled?(ECHO, :us)
|
|
|
|
- when BINARY
|
|
|
|
- @pstack.binary_on = enabled?(BINARY, :us)
|
|
|
|
- when ZMP
|
|
|
|
- @pstack.zmp_on = enabled?(ZMP, :us)
|
|
|
|
- end
|
|
|
|
- set_mode(:normal)
|
|
|
|
- when SB
|
|
|
|
- @sc.unscan
|
|
|
|
- break if @sc.check_until(/#{IAC.chr}#{SE.chr}/).nil?
|
|
|
|
- @sc.get_byte
|
|
|
|
- opt = @sc.get_byte
|
|
|
|
- data = @sc.scan_until(/#{IAC.chr}#{SE.chr}/).chop.chop
|
|
|
|
- parse_subneg(opt.getbyte(0),data)
|
|
|
|
- set_mode(:normal)
|
|
|
|
- else
|
|
|
|
- log.debug("(#{@pstack.conn.object_id}) Unknown Telnet command - #{b.getbyte(0)}")
|
|
|
|
- set_mode(:normal)
|
|
|
|
- end
|
|
|
|
- end
|
|
|
|
- end # while b
|
|
|
|
-
|
|
|
|
- @sc = nil if @sc.eos?
|
|
|
|
- buf
|
|
|
|
- end
|
|
|
|
-
|
|
|
|
- # The filter_out method filters output data
|
|
|
|
- # [+str+] The string to be processed
|
|
|
|
- # [+return+] The filtered data
|
|
|
|
- def filter_out(str)
|
|
|
|
- return '' if str.nil? || str.empty?
|
|
|
|
- if !@pstack.binary_on
|
|
|
|
- str.gsub!(/\n/, "\r\n")
|
|
|
|
- end
|
|
|
|
- str
|
|
|
|
- end
|
|
|
|
-
|
|
|
|
- ###### Custom public methods
|
|
|
|
-
|
|
|
|
- # Test to see if option is enabled
|
|
|
|
- # [+opt+] The Telnet option code
|
|
|
|
- # [+who+] The side to check :us or :him
|
|
|
|
- def enabled?(opt, who)
|
|
|
|
- option(opt)
|
|
|
|
- e = @state[opt].send(who)
|
|
|
|
- e == :yes ? true : false
|
|
|
|
- end
|
|
|
|
-
|
|
|
|
- # Test to see which state we prefer this option to be in
|
|
|
|
- # [+opt+] The Telnet option code
|
|
|
|
- def desired?(opt)
|
|
|
|
- st = @wopts[opt]
|
|
|
|
- st = false if st.nil?
|
|
|
|
- st
|
|
|
|
- end
|
|
|
|
-
|
|
|
|
- # Handle server-side echo
|
|
|
|
- # [+ch+] character string to echo
|
|
|
|
- def echo(ch)
|
|
|
|
- return if @server.service_type == :client # Never echo for server when client
|
|
|
|
- # Remove this if it makes sense for peer to peer
|
|
|
|
- if @pstack.echo_on
|
|
|
|
- if @pstack.hide_on && ch.getbyte(0) != CR
|
|
|
|
- @pstack.conn.sock.send('*',0)
|
|
|
|
- else
|
|
|
|
- @pstack.conn.sock.send(ch,0)
|
|
|
|
|
|
+
|
|
|
|
+
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
-
|
|
|
|
- # Negotiate starting wanted options that imply subnegotation
|
|
|
|
- # So far only terminal type
|
|
|
|
- def init_subneg
|
|
|
|
- return if @init_tries > 20
|
|
|
|
- @init_tries += 1
|
|
|
|
- @wopts.each_key do |opt|
|
|
|
|
- next if !@sneg_opts.include?(opt)
|
|
|
|
- log.debug("(#{@pstack.conn.object_id}) Subnegotiation attempt for option #{opt}.")
|
|
|
|
- case opt
|
|
|
|
- when TTYPE
|
|
|
|
- who = :him
|
|
|
|
- else
|
|
|
|
- who = :us
|
|
|
|
- end
|
|
|
|
- if desired?(opt) == enabled?(opt, who)
|
|
|
|
- case opt
|
|
|
|
- when TTYPE
|
|
|
|
- @pstack.conn.sendmsg(IAC.chr + SB.chr + TTYPE.chr + 1.chr + IAC.chr + SE.chr)
|
|
|
|
- when ZMP
|
|
|
|
- log.info("(#{@pstack.conn.object_id}) ZMP successfully negotiated." )
|
|
|
|
- @pstack.conn.sendmsg("#{IAC.chr}#{SB.chr}#{ZMP.chr}" +
|
|
|
|
- "zmp.check#{NUL.chr}color.#{NUL.chr}" +
|
|
|
|
- "#{IAC.chr}#{SE.chr}")
|
|
|
|
- @pstack.conn.sendmsg("#{IAC.chr}#{SB.chr}#{ZMP.chr}" +
|
|
|
|
- "zmp.ident#{NUL.chr}WOE#{NUL.chr}#{Version}#{NUL.chr}A mud based on the TeensyMUD server#{NUL.chr}" +
|
|
|
|
- "#{IAC.chr}#{SE.chr}")
|
|
|
|
- @pstack.conn.sendmsg("#{IAC.chr}#{SB.chr}#{ZMP.chr}" +
|
|
|
|
- "zmp.ping#{NUL.chr}" +
|
|
|
|
- "#{IAC.chr}#{SE.chr}")
|
|
|
|
- @pstack.conn.sendmsg("#{IAC.chr}#{SB.chr}#{ZMP.chr}" +
|
|
|
|
- "zmp.input#{NUL.chr}\n I see you support...\n ZMP protocol\n#{NUL.chr}" +
|
|
|
|
- "#{IAC.chr}#{SE.chr}")
|
|
|
|
- end
|
|
|
|
- @sneg_opts.delete(opt)
|
|
|
|
- end
|
|
|
|
- end
|
|
|
|
-
|
|
|
|
- if @init_tries > 20
|
|
|
|
- log.debug("(#{@pstack.conn.object_id}) Telnet init_subneg option - Timed out after #{@init_tries} tries.")
|
|
|
|
- @sneg_opts = []
|
|
|
|
- @pstack.conn.set_initdone
|
|
|
|
- if !@pstack.terminal or @pstack.terminal.empty?
|
|
|
|
- @pstack.terminal = "dumb"
|
|
|
|
- end
|
|
|
|
|
|
+
|
|
|
|
+ # Sends unescaped data to client, possibly compressing it if needed
|
|
|
|
+ def send_raw(buf)
|
|
|
|
+ if @compress
|
|
|
|
+ zbuf = Zlib.deflate(buf)
|
|
|
|
+ else
|
|
|
|
+ zbuf = buf
|
|
end
|
|
end
|
|
|
|
+ @client.send_data(zbuf)
|
|
end
|
|
end
|
|
-
|
|
|
|
- def send_naws
|
|
|
|
- return if !enabled?(NAWS, :us)
|
|
|
|
- ts = @pstack.query(:termsize)
|
|
|
|
- data = [ts[0]].pack('n') + [ts[1]].pack('n')
|
|
|
|
- data.gsub!(/#{IAC}/, IAC.chr + IAC.chr) # 255 needs to be doubled
|
|
|
|
- @pstack.conn.sendmsg(IAC.chr + SB.chr + NAWS.chr + data + IAC.chr + SE.chr)
|
|
|
|
- end
|
|
|
|
-
|
|
|
|
-private
|
|
|
|
- ###### Private methods
|
|
|
|
-
|
|
|
|
- def getopts(wopts)
|
|
|
|
- # supported options
|
|
|
|
- wopts.each do |op|
|
|
|
|
- case op
|
|
|
|
- when :ttype
|
|
|
|
- @wopts[TTYPE] = true
|
|
|
|
- when :echo
|
|
|
|
- @wopts[ECHO] = true
|
|
|
|
- when :sga
|
|
|
|
- @wopts[SGA] = true
|
|
|
|
- when :naws
|
|
|
|
- @wopts[NAWS] = true
|
|
|
|
- when :eorec
|
|
|
|
- @wopts[EOREC] = true
|
|
|
|
- when :binary
|
|
|
|
- @wopts[BINARY] = true
|
|
|
|
- when :zmp
|
|
|
|
- @wopts[ZMP] = true
|
|
|
|
- end
|
|
|
|
- end
|
|
|
|
|
|
+
|
|
|
|
+ # Send data to client (escapes IAC bytes)
|
|
|
|
+ def send_escaped(buf)
|
|
|
|
+ iac = TELNET_IAC.chr
|
|
|
|
+ self.send_raw(buf.gsub("#{iac}", "#{iac}#{iac}")
|
|
end
|
|
end
|
|
-
|
|
|
|
- # parse the subnegotiation data and save it
|
|
|
|
- # [+opt+] The Telnet option found
|
|
|
|
- # [+data+] The data found between SB OPTION and IAC SE
|
|
|
|
- def parse_subneg(opt,data)
|
|
|
|
- data.gsub!(/#{IAC}#{IAC}/, IAC.chr) # 255 needs to be undoubled from all data
|
|
|
|
- case opt
|
|
|
|
- when NAWS
|
|
|
|
- @pstack.twidth = data[0..1].unpack('n')[0]
|
|
|
|
- @pstack.theight = data[2..3].unpack('n')[0]
|
|
|
|
- @pstack.conn.publish(:termsize)
|
|
|
|
- log.debug("(#{@pstack.conn.object_id}) Terminal width #{@pstack.twidth} / height #{@pstack.theight}")
|
|
|
|
- when TTYPE
|
|
|
|
- if data.getbyte(0) == 0
|
|
|
|
- log.debug("(#{@pstack.conn.object_id}) Terminal type - #{data[1..-1]}")
|
|
|
|
- if !@ttype.include?(data[1..-1])
|
|
|
|
- # short-circuit choice because of Zmud
|
|
|
|
- if data[1..-1].downcase == 'zmud'
|
|
|
|
- @ttype << data[1..-1]
|
|
|
|
- @pstack.terminal = 'zmud'
|
|
|
|
- log.debug("(#{@pstack.conn.object_id}) Terminal choice - #{@pstack.terminal} in list #{@ttype.inspect}")
|
|
|
|
- end
|
|
|
|
- # short-circuit choice because of Windows telnet client
|
|
|
|
- if data[1..-1].downcase == 'vt100'
|
|
|
|
- @ttype << data[1..-1]
|
|
|
|
- @pstack.terminal = 'vt100'
|
|
|
|
- log.debug("(#{@pstack.conn.object_id}) Terminal choice - #{@pstack.terminal} in list #{@ttype.inspect}")
|
|
|
|
- end
|
|
|
|
- return if @pstack.terminal
|
|
|
|
- @ttype << data[1..-1]
|
|
|
|
- @pstack.conn.sendmsg(IAC.chr + SB.chr + TTYPE.chr + 1.chr + IAC.chr + SE.chr)
|
|
|
|
- else
|
|
|
|
- return if @pstack.terminal
|
|
|
|
- choose_terminal
|
|
|
|
- end
|
|
|
|
- elsif data.getbyte(0) == 1 # send - should only be called by :client
|
|
|
|
- return if !@pstack.terminal
|
|
|
|
- @pstack.conn.sendmsg(IAC.chr + SB.chr + TTYPE.chr + 0.chr + @pstack.terminal + IAC.chr + SE.chr)
|
|
|
|
- end
|
|
|
|
- when ZMP
|
|
|
|
- args = data.split("\0")
|
|
|
|
- cmd = args.shift
|
|
|
|
- handle_zmp(cmd,args)
|
|
|
|
- end
|
|
|
|
|
|
+
|
|
|
|
+ # Send negotiation bytes
|
|
|
|
+
|
|
|
|
+/* send negotiation bytes */
|
|
|
|
+ def send_negotiate(cmd, telopt)
|
|
|
|
+ bytes = ""
|
|
|
|
+ bytes << TELNET_IAC
|
|
|
|
+ bytes << cmd
|
|
|
|
+ bytes << telopt
|
|
|
|
+ send_raw(bytes)
|
|
end
|
|
end
|
|
-
|
|
|
|
- # Pick a preferred terminal
|
|
|
|
- # Order is vt100, vt999, ansi, xterm, or a recognized custom client
|
|
|
|
- # Should not pick vtnt as we dont handle it
|
|
|
|
- def choose_terminal
|
|
|
|
- if @ttype.empty?
|
|
|
|
- @pstack.terminal = "dumb"
|
|
|
|
- end
|
|
|
|
-
|
|
|
|
- # Pick most capable from list of terminals
|
|
|
|
- @pstack.terminal = @ttype.find {|t| t =~ /mushclient/i } if !@pstack.terminal
|
|
|
|
- @pstack.terminal = @ttype.find {|t| t =~ /simplemu/i } if !@pstack.terminal
|
|
|
|
- @pstack.terminal = @ttype.find {|t| t =~ /(zmud).*/i } if !@pstack.terminal
|
|
|
|
- @pstack.terminal = @ttype.find {|t| t =~ /linux/i } if !@pstack.terminal
|
|
|
|
- @pstack.terminal = @ttype.find {|t| t =~ /cygwin/i } if !@pstack.terminal
|
|
|
|
- @pstack.terminal = @ttype.find {|t| t =~ /(cons25).*/i } if !@pstack.terminal
|
|
|
|
- @pstack.terminal = @ttype.find {|t| t =~ /(xterm).*/i } if !@pstack.terminal
|
|
|
|
- @pstack.terminal = @ttype.find {|t| t =~ /(vt)[-]?100/i } if !@pstack.terminal
|
|
|
|
- @pstack.terminal = @ttype.find {|t| t =~ /(vt)[-]?\d+/i } if !@pstack.terminal
|
|
|
|
- @pstack.terminal = @ttype.find {|t| t =~ /(ansi).*/i } if !@pstack.terminal
|
|
|
|
-
|
|
|
|
- if @pstack.terminal && @ttype.last != @pstack.terminal # short circuit retraversal of options
|
|
|
|
- @ttype.each do |t|
|
|
|
|
- @pstack.conn.sendmsg(IAC.chr + SB.chr + TTYPE.chr + 1.chr + IAC.chr + SE.chr)
|
|
|
|
- break if t == @pstack.terminal
|
|
|
|
- end
|
|
|
|
- elsif @ttype.last != @pstack.terminal
|
|
|
|
- @pstack.terminal = 'dumb'
|
|
|
|
- end
|
|
|
|
-
|
|
|
|
- @pstack.terminal.downcase!
|
|
|
|
-
|
|
|
|
- # translate certain terminals to something meaningful
|
|
|
|
- case @pstack.terminal
|
|
|
|
- when /cygwin/i, /cons25/i, /linux/i, /dec-vt/i
|
|
|
|
- @pstack.terminal = 'vt100'
|
|
|
|
- when /ansis/i then
|
|
|
|
- @pstack.terminal = 'ansi'
|
|
|
|
- end
|
|
|
|
- log.debug("(#{@pstack.conn.object_id}) Terminal set to - #{@pstack.terminal} from list #{@ttype.inspect}")
|
|
|
|
|
|
+
|
|
|
|
+ # Check if we support a particular telopt;
|
|
|
|
+ def us_support(telopt)
|
|
|
|
+ have = @telopts[telopt]
|
|
|
|
+ return false unless have
|
|
|
|
+ return (have.telopt == telopt) && have.us
|
|
end
|
|
end
|
|
-
|
|
|
|
- # Get current parse mode
|
|
|
|
- # [+return+] The current parse mode
|
|
|
|
- def mode?
|
|
|
|
- return @mode
|
|
|
|
|
|
+
|
|
|
|
+ # Check if the remote supports a telopt
|
|
|
|
+ def him_support(telopt)
|
|
|
|
+ have = @telopts[telopt]
|
|
|
|
+ return false unless have
|
|
|
|
+ return (have.telopt == telopt) && have.him
|
|
end
|
|
end
|
|
-
|
|
|
|
- # set current parse mode
|
|
|
|
- # [+m+] Mode to set it to
|
|
|
|
- def set_mode(m)
|
|
|
|
- @mode = m
|
|
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ # retrieve RFC1143 option state
|
|
|
|
+ def rfc1143_get(telopt)
|
|
|
|
+ @rfc1143[telopt]
|
|
end
|
|
end
|
|
-
|
|
|
|
- # Creates an option entry in our state table and sets its initial state
|
|
|
|
- def option(opt)
|
|
|
|
- return if @state.key?(opt)
|
|
|
|
- o = OpenStruct.new
|
|
|
|
- o.us = :no
|
|
|
|
- o.him = :no
|
|
|
|
- o.usq = :empty
|
|
|
|
- o.himq = :empty
|
|
|
|
- @state[opt] = o
|
|
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ # save RFC1143 option state
|
|
|
|
+ def rfc1143_set(telopt, us, him)
|
|
|
|
+ agree = we_support(telopt)
|
|
|
|
+ @rfc1143[telopt] = RFC1143.new(telopt, us, him, agree)
|
|
|
|
+ return @rfc1143[telopt]
|
|
end
|
|
end
|
|
-
|
|
|
|
- # Ask the client to enable or disable an option.
|
|
|
|
- #
|
|
|
|
- # [+opt+] The option code
|
|
|
|
- # [+enable+] true for enable, false for disable
|
|
|
|
- def ask_him(opt, enable)
|
|
|
|
- log.debug("(#{@pstack.conn.object_id}) Requested Telnet option #{opt.to_s} set to #{enable.to_s}")
|
|
|
|
- initiate(opt, enable, :him)
|
|
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ # RFC1143 telnet option negotiation helper
|
|
|
|
+ def rfc1143_negotiate_will(rfc1143)
|
|
|
|
+ return rfc1143.handle_will
|
|
end
|
|
end
|
|
-
|
|
|
|
- # Offer the server to enable or disable an option
|
|
|
|
- #
|
|
|
|
- # [+opt+] The option code
|
|
|
|
- # [+enable+] true for enable, false for disable
|
|
|
|
- def offer_us(opt, enable)
|
|
|
|
- log.debug("(#{@pstack.conn.object_id}) Offered Telnet option #{opt.to_s} set to #{enable.to_s}")
|
|
|
|
- initiate(opt, enable, :us)
|
|
|
|
|
|
+
|
|
|
|
+ # RFC1143 telnet option negotiation helper
|
|
|
|
+ def rfc1143_negotiate_wont(rfc1143)
|
|
|
|
+ return rfc1143.handle_wont
|
|
end
|
|
end
|
|
-
|
|
|
|
- # Initiate a request to client. Called by ask_him or offer_us.
|
|
|
|
- #
|
|
|
|
- # [+opt+] The option code
|
|
|
|
- # [+enable+] true for enable, false for disable
|
|
|
|
- # [+who+] :him if asking client, :us if server offering
|
|
|
|
- def initiate(opt, enable, who)
|
|
|
|
- option(opt)
|
|
|
|
-
|
|
|
|
- case who
|
|
|
|
- when :him
|
|
|
|
- willdo = DO.chr
|
|
|
|
- wontdont = DONT.chr
|
|
|
|
- whoq = :himq
|
|
|
|
- when :us
|
|
|
|
- willdo = WILL.chr
|
|
|
|
- wontdont = WONT.chr
|
|
|
|
- whoq = :usq
|
|
|
|
- else
|
|
|
|
- # Error
|
|
|
|
- end
|
|
|
|
-
|
|
|
|
- case @state[opt].send(who)
|
|
|
|
- when :no
|
|
|
|
- if enable
|
|
|
|
- @state[opt].send("#{who}=", :wantyes)
|
|
|
|
- @pstack.conn.sendmsg(IAC.chr + willdo + opt.chr)
|
|
|
|
- else
|
|
|
|
- # Error already disabled
|
|
|
|
- log.error("(#{@pstack.conn.object_id}) Telnet negotiation: option #{opt.to_s} already disabled")
|
|
|
|
- end
|
|
|
|
- when :yes
|
|
|
|
- if enable
|
|
|
|
- # Error already enabled
|
|
|
|
- log.error("(#{@pstack.conn.object_id}) Telnet negotiation: option #{opt.to_s} already enabled")
|
|
|
|
- else
|
|
|
|
- @state[opt].send("#{who}=", :wantno)
|
|
|
|
- @pstack.conn.sendmsg(IAC.chr + wontdont + opt.chr)
|
|
|
|
- end
|
|
|
|
- when :wantno
|
|
|
|
- if enable
|
|
|
|
- case @state[opt].send(whoq)
|
|
|
|
- when :empty
|
|
|
|
- @state[opt].send("#{whoq}=", :opposite)
|
|
|
|
- when :opposite
|
|
|
|
- # Error already queued enable request
|
|
|
|
- log.error("(#{@pstack.conn.object_id}) Telnet negotiation: option #{opt.to_s} already queued enable request")
|
|
|
|
- end
|
|
|
|
- else
|
|
|
|
- case @state[opt].send(whoq)
|
|
|
|
- when :empty
|
|
|
|
- # Error already negotiating for disable
|
|
|
|
- log.error("(#{@pstack.conn.object_id}) Telnet negotiation: option #{opt.to_s} already negotiating for disable")
|
|
|
|
- when :opposite
|
|
|
|
- @state[opt].send("#{whoq}=", :empty)
|
|
|
|
- end
|
|
|
|
- end
|
|
|
|
- when :wantyes
|
|
|
|
- if enable
|
|
|
|
- case @state[opt].send(whoq)
|
|
|
|
- when :empty
|
|
|
|
- #Error already negotiating for enable
|
|
|
|
- log.error("(#{@pstack.conn.object_id}) Telnet negotiation: option #{opt.to_s} already negotiating for enable")
|
|
|
|
- when :opposite
|
|
|
|
- @state[opt].send("#{whoq}=", :empty)
|
|
|
|
- end
|
|
|
|
- else
|
|
|
|
- case @state[opt].send(whoq)
|
|
|
|
- when :empty
|
|
|
|
- @state[opt].send("#{whoq}=", :opposite)
|
|
|
|
- when :opposite
|
|
|
|
- #Error already queued for disable request
|
|
|
|
- log.error("(#{@pstack.conn.object_id}) Telnet negotiation: option #{opt.to_s} already queued for disable request")
|
|
|
|
- end
|
|
|
|
- end
|
|
|
|
- end
|
|
|
|
|
|
+
|
|
|
|
+ # RFC1143 telnet option negotiation helper
|
|
|
|
+ def rfc1143_negotiate_do(rfc1143)
|
|
|
|
+ return rfc1143.handle_do
|
|
end
|
|
end
|
|
-
|
|
|
|
- # Client replies WILL or WONT
|
|
|
|
- #
|
|
|
|
- # [+opt+] The option code
|
|
|
|
- # [+enable+] true for WILL answer, false for WONT answer
|
|
|
|
- def replies_him(opt, enable)
|
|
|
|
- log.debug("(#{@pstack.conn.object_id}) Client replies to Telnet option #{opt.to_s} set to #{enable.to_s}")
|
|
|
|
- response(opt, enable, :him)
|
|
|
|
|
|
+
|
|
|
|
+ # RFC1143 telnet option negotiation helper
|
|
|
|
+ def rfc1143_negotiate_dont(rfc1143)
|
|
|
|
+ return rfc1143.handle_dont
|
|
end
|
|
end
|
|
-
|
|
|
|
- # Client requests DO or DONT
|
|
|
|
- #
|
|
|
|
- # [+opt+] The option code
|
|
|
|
- # [+enable+] true for DO request, false for DONT request
|
|
|
|
- def requests_us(opt, enable)
|
|
|
|
- log.debug("(#{@pstack.conn.object_id}) Client requests Telnet option #{opt.to_s} set to #{enable.to_s}")
|
|
|
|
- response(opt, enable, :us)
|
|
|
|
|
|
+
|
|
|
|
+ # RFC1143 telnet option negotiation helper
|
|
|
|
+ def rfc1143_negotiate(telopt)
|
|
|
|
+ q = rfc1143_get(telopt)
|
|
|
|
+ return nil, nil unless q
|
|
|
|
+
|
|
|
|
+ case @state
|
|
|
|
+ when :will
|
|
|
|
+ return rfc1143_negotiate_will(q)
|
|
|
|
+ when :wont
|
|
|
|
+ return rfc1143_negotiate_wont(q)
|
|
|
|
+ when :do
|
|
|
|
+ return rfc1143_negotiate_do(q)
|
|
|
|
+ when :dont
|
|
|
|
+ return rfc1143_negotiate_dont(q)
|
|
|
|
+ end
|
|
end
|
|
end
|
|
-
|
|
|
|
- # Handle client response. Called by requests_us or replies_him
|
|
|
|
- #
|
|
|
|
- # [+opt+] The option code
|
|
|
|
- # [+enable+] true for WILL answer, false for WONT answer
|
|
|
|
- # [+who+] :him if client replies, :us if client requests
|
|
|
|
- def response(opt, enable, who)
|
|
|
|
- option(opt)
|
|
|
|
-
|
|
|
|
- case who
|
|
|
|
- when :him
|
|
|
|
- willdo = DO.chr
|
|
|
|
- wontdont = DONT.chr
|
|
|
|
- whoq = :himq
|
|
|
|
- when :us
|
|
|
|
- willdo = WILL.chr
|
|
|
|
- wontdont = WONT.chr
|
|
|
|
- whoq = :usq
|
|
|
|
|
|
+
|
|
|
|
+ def do_negotiate(telopt)
|
|
|
|
+ res, arg = rfc1143_negotiate(telopt)
|
|
|
|
+ return unless res
|
|
|
|
+ if res == :error
|
|
|
|
+ log_error(arg)
|
|
else
|
|
else
|
|
- # Error
|
|
|
|
- end
|
|
|
|
-
|
|
|
|
- case @state[opt].send(who)
|
|
|
|
- when :no
|
|
|
|
- if enable
|
|
|
|
- if desired?(opt)
|
|
|
|
- # If we agree
|
|
|
|
- @state[opt].send("#{who}=", :yes)
|
|
|
|
- @pstack.conn.sendmsg(IAC.chr + willdo + opt.chr)
|
|
|
|
- log.debug("(#{@pstack.conn.object_id}) Telnet negotiation: agreed to enable option #{opt}")
|
|
|
|
- else
|
|
|
|
- # If we disagree
|
|
|
|
- @pstack.conn.sendmsg(IAC.chr + wontdont + opt.chr)
|
|
|
|
- log.debug("(#{@pstack.conn.object_id}) Telnet negotiation: disagreed to enable option #{opt}")
|
|
|
|
- end
|
|
|
|
- else
|
|
|
|
- # Ignore
|
|
|
|
- end
|
|
|
|
- when :yes
|
|
|
|
- if enable
|
|
|
|
- # Ignore
|
|
|
|
- else
|
|
|
|
- @state[opt].send("#{who}=", :no)
|
|
|
|
- @pstack.conn.sendmsg(IAC.chr + wontdont + opt.chr)
|
|
|
|
- end
|
|
|
|
- when :wantno
|
|
|
|
- if enable
|
|
|
|
- case @state[opt].send(whoq)
|
|
|
|
- when :empty
|
|
|
|
- #Error DONT/WONT answered by WILL/DO
|
|
|
|
- @state[opt].send("#{who}=", :no)
|
|
|
|
- when :opposite
|
|
|
|
- #Error DONT/WONT answered by WILL/DO
|
|
|
|
- @state[opt].send("#{who}=", :yes)
|
|
|
|
- @state[opt].send("#{whoq}=", :empty)
|
|
|
|
- end
|
|
|
|
- log.error("(#{@pstack.conn.object_id}) Telnet negotiation: option #{opt.to_s} DONT/WONT answered by WILL/DO")
|
|
|
|
- else
|
|
|
|
- case @state[opt].send(whoq)
|
|
|
|
- when :empty
|
|
|
|
- @state[opt].send("#{who}=", :no)
|
|
|
|
- log.debug("(#{@pstack.conn.object_id}) Telnet negotiation: agreed to disable option #{opt}")
|
|
|
|
- when :opposite
|
|
|
|
- @state[opt].send("#{who}=", :wantyes)
|
|
|
|
- @state[opt].send("#{whoq}=", :empty)
|
|
|
|
- @pstack.conn.sendmsg(IAC.chr + willdo + opt.chr)
|
|
|
|
- end
|
|
|
|
- end
|
|
|
|
- when :wantyes
|
|
|
|
- if enable
|
|
|
|
- case @state[opt].send(whoq)
|
|
|
|
- when :empty
|
|
|
|
- @state[opt].send("#{who}=", :yes)
|
|
|
|
- log.debug("(#{@pstack.conn.object_id}) Telnet negotiation: agreed to enable option #{opt}")
|
|
|
|
- when :opposite
|
|
|
|
- @state[opt].send("#{who}=", :wantno)
|
|
|
|
- @state[opt].send("#{whoq}=", :empty)
|
|
|
|
- @pstack.conn.sendmsg(IAC.chr + wontdont + opt.chr)
|
|
|
|
- end
|
|
|
|
- else
|
|
|
|
- case @state[opt].send(whoq)
|
|
|
|
- when :empty
|
|
|
|
- @state[opt].send("#{who}=", :no)
|
|
|
|
- log.debug("(#{@pstack.conn.object_id}) Telnet negotiation: agreed to disable option #{opt}")
|
|
|
|
- when :opposite
|
|
|
|
- @state[opt].send("#{who}=", :no)
|
|
|
|
- @state[opt].send("#{whoq}=", :empty)
|
|
|
|
- end
|
|
|
|
- end
|
|
|
|
|
|
+ send_negotiate(res, arg)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
-
|
|
|
|
- def handle_zmp(cmd,args)
|
|
|
|
- log.debug("(#{@pstack.conn.object_id}) ZMP command recieved - '#{cmd}' args: #{args.inspect}" )
|
|
|
|
- case cmd
|
|
|
|
- when "zmp.ping"
|
|
|
|
- @pstack.conn.sendmsg("#{IAC.chr}#{SB.chr}#{ZMP.chr}" +
|
|
|
|
- "zmp.time#{NUL.chr}#{Time.now.utc.strftime("%Y-%m-%d %H:%M:%S")}#{NUL.chr}" +
|
|
|
|
- "#{IAC.chr}#{SE.chr}")
|
|
|
|
- when "zmp.time"
|
|
|
|
- when "zmp.ident"
|
|
|
|
- # That's nice
|
|
|
|
- when "zmp.check"
|
|
|
|
- case args[0]
|
|
|
|
- when /zmp.*/
|
|
|
|
- # We support all 'zmp.' package and commands so..
|
|
|
|
- @pstack.conn.sendmsg("#{IAC.chr}#{SB.chr}#{ZMP.chr}" +
|
|
|
|
- "zmp.support#{NUL.chr}#{args[0]}{NUL.chr}" +
|
|
|
|
- "#{IAC.chr}#{SE.chr}")
|
|
|
|
- else
|
|
|
|
- @pstack.conn.sendmsg("#{IAC.chr}#{SB.chr}#{ZMP.chr}" +
|
|
|
|
- "zmp.no-support#{NUL.chr}#{args[0]}#{NUL.chr}" +
|
|
|
|
- "#{IAC.chr}#{SE.chr}")
|
|
|
|
- end
|
|
|
|
- when "zmp.support"
|
|
|
|
- when "zmp.no-support"
|
|
|
|
- when "zmp.input"
|
|
|
|
- # Now we just simply pass this whole load to the Character.parse
|
|
|
|
- # WARN: This means there is a possibility of out-of-order processing
|
|
|
|
- # of @inbuffer, though extremely unlikely.
|
|
|
|
- @pstack.conn.publish(args[0])
|
|
|
|
- end
|
|
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ # Called when server data should be filtered before being passed to the client
|
|
|
|
+ def server_to_client(data)
|
|
|
|
+
|
|
end
|
|
end
|
|
-
|
|
|
|
end
|
|
end
|