TeensyMud - 'A ruby mud server.'

Browse repository
back

telnetfilter.rb

#
# 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
#
class TelnetFilter < Filter
include ASCIICodes
include TelnetCodes

logger 'DEBUG'

# Initialize state of filter
#
# [+pstack+] The ProtocolStack associated with this filter
# [+server+] An optional hash of desired initial options
def initialize(pstack, server)
super(pstack)
@server = server
@wopts = {}
getopts(@server.service_negotiation)
@mode = :normal # Parse mode :normal, :cmd, :cr
@state = {}
@sc = nil
@sneg_opts = [ TTYPE, ZMP ] # supported options which imply an initial
# sub negotiation of options
@ttype = []
@init_tries = 0 # Number of tries at negotitating sub options
@synch = false
log.debug "telnet filter initialized - #{@init_tries}"
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)
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("TeensyMUD 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

# 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}TeensyMUD#{NUL.chr}#{Version}#{NUL.chr}A sexy mud 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
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
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
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}")
end

# Get current parse mode
# [+return+] The current parse mode
def mode?
return @mode
end

# set current parse mode
# [+m+] Mode to set it to
def set_mode(m)
@mode = m
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
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)
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)
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
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)
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)
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
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
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
end

end