Browse Source

Try Crystal for good measure, start the port from the old Ruby implementation.

Beoran 7 years ago
parent
commit
83770249d1

+ 184 - 0
lib/monolog.cr

@@ -0,0 +1,184 @@
+
+# Monolog, an easy to use logger for ruby 
+module Monolog 
+  
+  module Logger
+    attr_reader :data
+    
+    def initialize(data = nil)
+      @data       = data
+    end 
+    
+    def log(file, line, name, level, format, *args)
+    end
+    
+    def close
+      @data.close if @data && @data.respond_to?(:close)
+    end
+    
+  end
+  
+  class FileLogger 
+    include Logger
+    def initialize(filename)
+      @data = File.open(filename, "at")
+    end
+    
+    def log(file, line, name, format, *args)
+      @data.printf("%s: %s: %s: %d: ", Time.now.to_s, name, file, line)
+      @data.printf(format, *args)
+      @data.printf("\n")
+    end
+  end
+  
+  class StdinLogger < FileLogger
+    def initialize
+       @data = $stdin
+    end
+  end
+    
+  class StderrLogger < FileLogger
+    def initialize
+       @data = $stderr
+    end
+  end
+         
+  class Log 
+    attr_reader :loggers
+    attr_reader :levels
+    
+    def initialize
+      @loggers = []
+      @levels  = {} 
+    end
+    
+    def add_logger(logger)  
+      @loggers << logger
+    end
+    
+    def enable_level(name)
+      @levels[name.to_sym] = true 
+    end
+    
+    def disable_level(name)
+      @levels[name.to_sym] = false 
+    end
+    
+    def log_va(file, line, name, format, *args)
+      level = @levels[name.to_sym]
+      return nil unless level   
+      @loggers.each do | logger |
+        logger.log(file, line, name, format, *args)
+      end
+    end
+    
+    def close
+      @loggers.each do | logger |
+        logger.close()
+      end
+    end
+  end
+  
+  def self.setup
+    @log = Log.new
+  end
+  
+  def self.get_log
+    return @log
+  end
+  
+  def self.setup_all(name = nil, err = true, out = false) 
+    setup
+    add_stderr_logger if err
+    add_stdout_logger if out    
+    add_file_logger(name) if name
+    enable_level(:INFO)
+    enable_level(:WARNING)
+    enable_level(:ERROR)
+    enable_level(:FATAL)
+  end
+  
+  def self.enable_level(l)
+    @log ||= nil
+    return unless @log
+    @log.enable_level(l)
+  end
+
+  def self.disable_level(l)      
+    @log ||= nil
+    return unless @log
+    @log.disable_level(l)
+  end
+  
+  def self.add_logger(l)
+    @log ||= nil
+    return unless @log
+    @log.add_logger(l)
+  end
+  
+  def self.add_stdin_logger
+    self.add_logger(StdinLogger.new)
+  end
+  
+  def self.add_stderr_logger
+    self.add_logger(StderrLogger.new)
+  end
+  
+  def self.add_file_logger(filename = "log.log")
+    self.add_logger(FileLogger.new(filename))
+  end
+  
+  def self.close
+    @log ||= nil
+    return unless @log
+    @log.close
+  end
+
+  
+  def self.log_va(file, line, name, format, *args)
+    @log ||= nil
+    return unless @log
+    @log.log_va(file, line, name, format, *args)
+  end
+  
+  def log(name, format, * args)
+    file, line, fun = caller.first.to_s.split(':')
+    Monolog.log_va(file, line, name, format, *args)
+  end
+  
+  def log_error(format, *args)
+    file, line, fun = caller.first.to_s.split(':')
+    Monolog.log_va(file, line, :ERROR, format, *args)
+  end
+
+  def log_warning(format, *args)
+    file, line, fun = caller.first.to_s.split(':')
+    Monolog.log_va(file, line, :WARNING, format, *args)
+  end
+
+  def log_info(format, *args)
+    file, line, fun = caller.first.to_s.split(':')
+    Monolog.log_va(file, line, :INFO, format, *args)
+  end
+
+  def log_debug(format, *args)
+    file, line, fun = caller.first.to_s.split(':')
+    Monolog.log_va(file, line, :DEBUG, format, *args)
+  end
+  
+  def log_fatal(format, *args)
+    file, line, fun = caller.first.to_s.split(':')
+    Monolog.log_va(file, line, :FATAL, format, *args)
+  end
+  
+  alias error log_error
+  alias warn log_warning
+  alias info log_info
+  alias error log_error
+  
+  extend(self)
+ end
+ 
+ 
+ 
+ 

+ 264 - 0
lib/rfc1143.cr

@@ -0,0 +1,264 @@
+
+require_relative 'telnet/codes'
+
+
+# rfc1143 state machine for telnet protocol
+# Thehandle_ functions handle input,
+# the send_ functions are for sending negotiations
+class RFC1143
+  include Telnet::Codes
+
+  attr_reader :telopt
+  attr_reader :us
+  attr_reader :him
+  attr_reader :agree
+  
+  def initialize(to, u, h, a)
+    @telopt = to
+    @us     = u
+    @him    = h
+    @agree  = a
+  end
+
+=begin
+ EXAMPLE STATE MACHINE
+ FOR THE Q METHOD OF IMPLEMENTING TELNET OPTION NEGOTIATION
+
+    There are two sides, we (us) and he (him).  We keep four
+    variables:
+
+       us: state of option on our side (NO/WANTNO/WANTYES/YES)
+       usq: a queue bit (EMPTY/OPPOSITE) if us is WANTNO or WANTYES
+       him: state of option on his side
+       himq: a queue bit if him is WANTNO or WANTYES
+
+    An option is enabled if and only if its state is YES.  Note that
+    us/usq and him/himq could be combined into two six-choice states.
+
+    "Error" below means that producing diagnostic information may be a
+    good idea, though it isn't required.
+
+    Upon receipt of WILL, we choose based upon him and himq:
+       NO            If we agree that he should enable, him=YES, send
+                     DO; otherwise, send DONT.
+       YES           Ignore.
+       WANTNO  EMPTY Error: DONT answered by WILL. him=NO.
+            OPPOSITE Error: DONT answered by WILL. him=YES*,
+                     himq=EMPTY.
+       WANTYES EMPTY him=YES.
+            OPPOSITE him=WANTNO, himq=EMPTY, send DONT.
+
+    * This behavior is debatable; DONT will never be answered by WILL
+      over a reliable connection between TELNETs compliant with this
+      RFC, so this was chosen (1) not to generate further messages,
+      because if we know we're dealing with a noncompliant TELNET we
+      shouldn't trust it to be sensible; (2) to empty the queue
+      sensibly.
+
+=end
+  def handle_will
+    case @us
+    when :no
+      if @agree
+        return TELNET_DO, @telopt
+      else
+        return TELNET_DONT, @telopt
+      end
+    when :yes
+      # ignore
+      return nil, nil
+    when :wantno
+      @him = :no
+      return :error, "DONT answered by WILL"
+    when :wantno_opposite
+      @him = :yes
+      return :error, "DONT answered by WILL"
+    when :wantyes
+      @him = :yes
+      return nil, nil
+    when :wantyes_opposite
+      @him = :wantno
+      return TELNET_DONT, @telopt
+    end
+  end
+  
+  
+=begin
+Upon receipt of WONT, we choose based upon him and himq:
+   NO            Ignore.
+   YES           him=NO, send DONT.
+   WANTNO  EMPTY him=NO.
+        OPPOSITE him=WANTYES, himq=NONE, send DO.
+   WANTYES EMPTY him=NO.*
+        OPPOSITE him=NO, himq=NONE.**
+
+* Here is the only spot a length-two queue could be useful; after
+  a WILL negotiation was refused, a queue of WONT WILL would mean
+  to request the option again. This seems of too little utility
+  and too much potential waste; there is little chance that the
+  other side will change its mind immediately.
+
+** Here we don't have to generate another request because we've
+   been "refused into" the correct state anyway.
+=end 
+  def handle_wont
+    case @us
+    when :no
+      return nil, nil
+    when :yes
+      @him = :no
+      return TELNET_DONT, @telopt
+    when :wantno
+      @him = :no
+      return nil, nil
+    when :wantno_opposite
+      @him = :wantyes
+      return TELNET_DO, @telopt
+    when :wantyes
+      @him = :no
+      return nil, nil
+    when :wantyes_opposite
+      @him = :no
+      return nil, nil
+    end
+  end   
+  
+=begin
+
+    If we decide to ask him to enable:
+       NO            him=WANTYES, send DO.
+       YES           Error: Already enabled.
+       WANTNO  EMPTY If we are queueing requests, himq=OPPOSITE;
+                     otherwise, Error: Cannot initiate new request
+                     in the middle of negotiation.
+            OPPOSITE Error: Already queued an enable request.
+       WANTYES EMPTY Error: Already negotiating for enable.
+            OPPOSITE himq=EMPTY.
+            
+    We handle the option on our side by the same procedures, with DO-
+    WILL, DONT-WONT, him-us, himq-usq swapped.         
+=end
+  def handle_do
+    case @him
+    when :no
+      @us = :wantyes
+      return TELNET_WILL, @telopt
+    when :yes
+      return :error, 'Already enabled'
+    when :wantno
+      # us = :wantno_opposite # only if "buffering", whatever that means.
+      return :error, 'Request in the middle of negotiation'
+    when :wantno_opposite
+      return :error, 'Already queued request'
+    when :wantyes
+      return :error, 'Already negotiating for enable'
+    when :wantyes_opposite
+      @us = :wantyes
+      return nil, nil
+    end
+  end   
+  
+=begin
+    If we decide to ask him to disable:
+       NO            Error: Already disabled.
+       YES           him=WANTNO, send DONT.
+       WANTNO  EMPTY Error: Already negotiating for disable.
+            OPPOSITE himq=EMPTY.
+       WANTYES EMPTY If we are queueing requests, himq=OPPOSITE;
+                     otherwise, Error: Cannot initiate new request
+                     in the middle of negotiation.
+            OPPOSITE Error: Already queued a disable request.
+
+    We handle the option on our side by the same procedures, with DO-
+    WILL, DONT-WONT, him-us, himq-usq swapped.
+=end
+  def handle_dont
+    case @him
+    when :no
+      return :error, 'Already disabled'
+    when :yes
+      @us = :wantno
+      return TELNET_WONT, @telopt
+    when :wantno
+      return :error, 'Already negotiating for disable'
+    when :wantno_opposite
+      @us = :wantno
+      return nil, nil
+    when :wantyes
+      # us = :wantno_opposite # only if "buffering", whatever that means.
+      return :error, 'Request in the middle of negotiation'
+    when :wantyes_opposite
+      return :error, 'Already queued disable request'
+    end
+  end
+  
+  # advertise willingess to support an option 
+  def send_will
+    case @us
+    when :no
+      @us = :wantyes
+      return TELNET_WILL, @telopt
+    when :wantno
+      @us = :wantno_opposite
+    when :wantyes_opposite
+      @us = :wantyes
+    else
+      return nil, nil
+    end
+  end
+  
+  # force turn-off of locally enabled option
+  def send_wont
+    case @us
+    when :yes
+      @us = :wantno
+      return TELNET_WONT, @telopt
+    when :wantno_opposite
+      @us = :wantno
+      return nil, nil
+    when :wantyes
+      @us = :wantyes_opposite
+      return nil, nil
+    else
+      return nil, nil
+    end
+  end   
+
+  # ask remote end to enable an option
+  def send_do
+    case @him
+    when :no
+      @him = :wantyes
+      return TELNET_DO, @telopt
+    when :wantno
+      @him = :wantno_opposite
+      return nil, nil
+    when :wantyes_opposite
+      @us = :wantyes
+      return nil, nil
+    else
+      return nil, nil
+    end
+  end
+
+
+  # demand remote end disable an option
+  def send_dont
+    case @him
+    when :yes
+      @him = :wantno
+      return TELNET_DONT, @telopt
+    when :wantno_opposite
+      @him = :wantno
+      return nil, nil
+    when :wantyes
+      @him = :wantyes_opposite
+    else
+      return nil, nil
+    end
+  end
+end
+  
+
+
+

+ 26 - 0
lib/security.cr

@@ -0,0 +1,26 @@
+#
+# Woe security related helper functions. 
+#
+
+
+CRYPT_MAKE_SALT_AID = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789./"
+
+# Generates salt for use by crypt.
+def crypt_make_salt  
+  c1 = CRYPT_MAKE_SALT_AID[rand(CRYPT_MAKE_SALT_AID.length)] 
+  c2 = CRYPT_MAKE_SALT_AID[rand(CRYPT_MAKE_SALT_AID.length)] 
+  return c1 + c2
+end
+
+# Crypt with salt generation.
+def crypt(pass, salt = nil) 
+  salt = crypt_make_salt unless salt
+  return pass.to_s.crypt(salt)
+end
+
+# Challenge crypt password trypass against the hash hash
+def crypt_challenge?(trypass, hash) 
+  salt = hash[0, 2]
+  tryhash = trypass.to_s.crypt(salt)
+  return tryhash == hash
+end

+ 204 - 0
lib/serdes.cr

@@ -0,0 +1,204 @@
+
+
+
+class Dir
+  def self.mkdir_p(name)
+    sub   = ""
+    parts = name.split('/').reject { |e| e.empty? }
+    parts.each do | part |
+      sub <<  "/#{part}"
+      mkdir sub rescue nil
+    end
+  end
+end
+
+
+
+# Module to help with serialization and deserialization of any type of data
+module Serdes
+  
+  module ClassMethods
+    def serdes_add_to_fields(name, type = nil)
+      @serdes_fields ||= []
+      info = { :name => name, :type => type }
+      @serdes_fields << info
+    end
+    
+    def serdes_reader(name, type = nil)
+      serdes_add_to_fields(name, type)
+      attr_reader(name)
+    end
+    
+    def serdes_writer(name)
+      serdes_add_to_fields(name, type = nil)
+      attr_writer(name)
+    end
+    
+    def serdes_accessor(name, type = nil)
+      serdes_add_to_fields(name, type)
+      attr_accessor(name)
+    end
+    
+    def serdes_fields()
+      @serdes_fields ||= []
+      return @serdes_fields
+    end
+    
+    
+    def serdes_register(obj)
+      @serdes_loaded ||= {}
+      @serdes_loaded[obj.id] = obj
+    end
+    
+    def serdes_forget(id)
+      @serdes_loaded ||= {}
+      @serdes_loaded.delete(id)
+    end
+    
+    def serdes_loaded()
+      @serdes_loaded ||= {}
+      return @serdes_loaded
+    end
+    
+    def serdes_get(id)
+      @serdes_loaded ||= {}
+      return @serdes_loaded[id.to_sym]
+    end
+
+    def serdes_load_one(id)
+      return nil unless id && !id.empty?
+      
+      full_name = Serdes.serdes_full_for(self, id)
+      data, errors  = Sitef.load_filename(full_name)
+      unless data
+        # log_error(errors.join(", "))
+        return nil
+      end
+      
+      eldat = data.select do |el|
+        el['id'].to_s == id.to_s
+      end
+      return nil unless eldat
+
+      eldat = eldat.first
+      return nil unless eldat
+      
+      typedat = {}
+      self.serdes_fields.each do |info|
+        name  = info[:name]
+        type  = info[:type]
+        value = eldat[name.to_s]
+        
+        typevalue = nil
+        
+        if type
+          if type.respond_to?(:serdes_load)
+            typevalue = type.serdes_load(value)
+          elsif Kernel.respond_to?(type.to_sym)
+            typevalue = Kernel.send(type.to_sym, value) rescue nil 
+          elsif type.respond_to(:new)
+            typevalue = type.new(value)
+          else 
+            typevalue = value
+          end
+        else
+          typevalue = value
+        end
+      
+        typedat[name] = typevalue
+      end
+      
+      obj = self.new(typedat)
+      return obj
+    end
+    
+    def serdes_fetch(id)
+      res = serdes_get(id)
+      return res if res
+      return serdes_load_one(id)
+    end
+    
+    alias :fetch    :serdes_fetch
+    alias :load_one :serdes_load_one
+    alias :get      :serdes_get
+    
+    def from_serdes(id)
+      return serdes_fetch(id)
+    end
+    
+    def to_serdes(value)
+      return value.id.to_s
+    end
+    
+    
+  end
+
+  # include callback, be sure to extend the class with the ClassMethods
+  def self.included(klass)
+    klass.extend(ClassMethods)
+  end
+  
+  def self.serdes_dir=(dir)
+    @serdes_dir = dir
+  end
+  
+  def self.serdes_dir
+    @serdes_dir ||= File.join(Dir.pwd, 'data', 'var')
+    @serdes_dir
+  end
+
+  
+  def self.serdes_dir_for(klass)
+    top = File.join(Serdes.serdes_dir,
+      klass.to_s.gsub('::', '/').downcase)
+    return top
+  end
+  
+  def self.serdes_file_for(id)
+    top = id.to_s.dup    
+    top << '.sitef'
+    return top 
+  end
+  
+  def self.serdes_full_for(klass, id)
+    tdir = serdes_dir_for(klass)
+    tfil = serdes_file_for(id)
+    return File.join(tdir, tfil)
+  end
+  
+
+  def serdes_data
+    data = {}
+    self.class.serdes_fields.each do |info|
+      name  = info[:name]
+      type  = info[:type]
+      type||= String
+      key   = "#{name}" 
+      value = "#{self.send(name.to_sym)}"
+      if type.respond_to?(:to_serdes)
+         wrapvalue = type.to_serdes(value)
+      else 
+         wrapvalue = value.to_s
+      end
+      data[key]    = wrapvalue
+    end
+    return data
+  end
+  
+  def save_one
+    Dir.mkdir_p Serdes.serdes_dir_for(self.class)
+    data = serdes_data
+    full_name = Serdes.serdes_full_for(self.class, self.id)
+    Sitef.save_filename(full_name, [ data ] )
+  end
+  
+  def initialize(fields = {}) 
+    fields.each  do |key, value|
+      p "Setting #{key} #{value}"
+      instance_variable_set("@#{key}", value)
+    end
+    self.class.serdes_register(self)
+  end
+
+end
+

+ 173 - 0
lib/sitef.cr

@@ -0,0 +1,173 @@
+
+# Sitef is a simple text format for serializing data to
+# It's intent is to be human readable and easy to 
+# use for multi line text.
+
+module Sitef
+  # All Sitef data is stored in files with one or more records.
+  # Records are separated by separated by at least 2 dashes on a line.
+  # Records contain key/value fields. The key starts in the first column
+  # with a : and is followed by a : the value starts after the second :
+  # A multiline key / value needs a key that starts with . and ends with .
+  # the end of the value is a  pair of dots .. by itself 
+  # Keys may not be nested, however, you could use spaces or dots, 
+  # or array indexes to emulate nexted keys. 
+  # A # at the start optionally after whitespace is a comment
+  # 
+  def self.parse_file(file)
+    lineno   = 0
+    results  = []
+    errors   = []
+    
+    record   = {}
+    key      = nil
+    value    = nil
+    until file.eof?
+      lineno     += 1 
+      line        = file.gets(256)
+      break if line.nil?
+      next if line.empty? 
+      # new record
+      if line[0,2] == '--' 
+        # Store last key used if any.
+        if key          
+          record[key] = value.chomp
+          key = nil
+        end  
+        results << record
+        record = {}
+      elsif line[0] == '#'
+      # Comments start with #
+      elsif line[0] == ':'
+      # a key/value pair
+      key, value  = line[1,line.size].split(':', 2)
+      record[key] = value.chomp
+      key = value = nil
+      elsif line[0, 2] == '..'
+      # end of multiline value 
+      record[key] = value.chomp
+      key = value = nil
+      elsif (line[0] == '.') && key.nil?
+      # Multiline key/value starts here (but is ignored 
+      # until .. is encountered)
+      key   = line[1, line.size]
+      key.chomp!
+      value = ""
+      # multiline value
+      elsif key
+          if line[0] == '\\'
+            # remove any escapes
+            line.slice!(0)
+          end
+          # continue the value
+          value << line
+      else
+          # Not in a key, sntax error.
+          errors << "#{lineno}: Don't know how to process line"
+      end      
+    end
+    # Store last key used if any.
+    if key      
+      record[key] = value.chomp
+    end  
+    # store last record 
+    results << record unless record.empty?
+    return results, errors
+  end  
+  
+  def self.load_filename(filename)
+    results, errors = nil, nil, nil;
+    file = File.open(filename, 'rt') rescue nil
+    return nil, ["Could not open #{filename}"] unless file
+    begin 
+      results, errors = parse_file(file)
+    ensure
+      file.close
+    end
+    return results, errors
+  end
+  
+  # Loads a Sitef fileas obejcts. Uses the ruby_klass atribute to load the object
+  # If that is missing, uses defklass
+  def self.load_objects(filename, defklass=nil)
+    results, errors = load_filename(filename)
+    p filename, results, errors
+    unless errors.nil? || errors.empty?
+      return nil, errors 
+    end
+    
+    objres = [] 
+    results.each do | result |
+      klassname = result['ruby_class'] || defklass
+      return nil unless klassname
+      klass = klassname.split('::').inject(Kernel) { |klass, name| klass.const_get(name) rescue nil } 
+      return nil unless klass
+      if klass.respond_to? :from_sitef
+        objres << klass.from_sitef(result)
+      else
+        objres << klass.new(result)
+      end      
+    end
+    return objres, errors    
+  end
+  
+  
+  # Saves a single field to a file in Sitef format.
+  def self.save_field(file, key, value)
+    if value.is_a? String
+      sval = value.dup
+    else
+      sval = value.to_s
+    end
+    if sval["\n"]
+      file.puts(".#{key}\n")
+      # Escape everything that could be misinterpreted with a \\
+      sval.gsub!(/\n([\.\-\:\#\\]+)/, "\n\\\\\\1")
+      sval.gsub!(/\A([\.\-\:\#\\]+)/, "\\\\\\1")
+      file.printf("%s", sval)
+      file.printf("\n..\n")
+    else
+      file.printf(":#{key}:#{sval}\n")
+    end
+  end
+  
+  def self.save_object(file, object, *fields)
+    save_field(file, :ruby_class, object.class.to_s)
+    fields.each do | field |
+      value = object.send(field.to_sym)
+      save_field(file, field, value)
+    end
+  end
+  
+  def self.save_record(file, record, *fields)
+    record.each do | key, value |
+      next if fields && !fields.empty? && !fields.member?(key)
+      save_field(file, key, value)
+    end
+  end
+
+  def self.save_file(file, records, *fields)
+    records.each do | record |
+      if record.is_a? Hash
+        save_record(file, record, *fields)
+      else 
+        save_object(file, record, *fields)
+      end
+      file.puts("--\n")
+    end
+  end
+  
+  def self.save_filename(filename, records, *fields)
+    results , errors = nil, nil
+    file = File.open(filename, 'wt')
+    return false, ["Could not open #{filename}"] unless file
+    begin 
+      save_file(file, records, *fields)
+    ensure
+      file.close
+    end
+    return true, []
+  end
+  
+end
+

+ 608 - 0
lib/telnet.cr

@@ -0,0 +1,608 @@
+require 'zlib'
+require_relative 'telnet/codes'
+require_relative 'rfc1143'
+require_relative 'monolog'
+
+
+
+# This Telnet class implements a subset of the Telnet protocol.
+#
+class Telnet
+  include Monolog
+  include Telnet::Codes
+
+  # 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)
+
+  attr_reader :telopts
+  
+  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
+    @zdeflate   = Zlib::Deflate.new() # Deflate stream for compression2 support.
+    @zinflate   = Zlib::Inflate.new() # Inflate stream for compression2 support.
+  end
+  
+  # Closes the telnet connection, send last compressed data if needed.
+  def close
+    if @compress 
+      zbuf = @zdeflate.flush(Zlib::FINISH)
+      @client.telnet_send_data(zbuf)
+    end
+    @zdeflate.close
+    @zinflate.close    
+  end
+  
+  # Send an event to the client to notify it of a state change or of data
+  def send_event(type, *data)
+    @client.telnet_event(type, *data)
+  end
+  
+  # Sends unescaped data to client, possibly compressing it if needed
+  def send_raw(buf)
+    if @compress
+      @zdeflate << buf
+      # for short messages the "compressed" stream wil actually be 
+      # bigger than the uncompressed one, but that's unavoidable
+      # due to the streaming nature of network connections.
+      zbuf = @zdeflate.flush(Zlib::SYNC_FLUSH)
+    else
+      zbuf = buf
+    end
+    # Don't use send_event here, since that's only for events received
+    @client.telnet_send_data(zbuf)
+  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
+  
+  # Send negotiation bytes
+  
+  # negotiation bytes 
+  def send_negotiate(cmd, telopt)
+    bytes = ""
+    bytes << TELNET_IAC
+    bytes << cmd
+    bytes << telopt
+    send_raw(bytes)
+  end  
+  
+  # 
+  
+  # Check if we support a particular telsopt using the RFC1143 state
+  def us_support(telopt)
+    have = @rfc1143[telopt]
+    return false unless have
+    return (have.telopt == telopt) && have.us == :yes 
+  end
+  
+  # Check if the remote supports a telopt (and it is enabled)
+  def him_support(telopt)
+    have = @rfc1143[telopt]
+    return false unless have
+    return (have.telopt == telopt) && have.him == :yes 
+  end
+  
+  # Set that we support an option (using the RFC1143 state)
+  def set_support(telopt, support=true, us = :no, him = :no)
+    rfc1143_set(telopt, support=true, us = :no, him = :no)
+  end
+   
+  # retrieve RFC1143 option state
+  def rfc1143_get(telopt)
+    @rfc1143[telopt]
+  end
+    
+  # save RFC1143 option state
+  def rfc1143_set(telopt, support=true, us = :no, him = :no)
+    agree = support
+    @rfc1143[telopt] = RFC1143.new(telopt, us, him, agree)
+    return @rfc1143[telopt]
+  end
+  
+  
+  # RFC1143 telnet option negotiation helper
+  def rfc1143_negotiate(telopt)
+    q = rfc1143_get(telopt)
+    return nil, nil unless q
+    
+    case @state
+    when :will
+      return q.handle_will 
+    when :wont
+      return q.handle_wont
+    when :do
+      return q.handle_do
+    when :dont
+      return q.handle_dont
+    end  
+  end
+  
+  # Performs a telnet negotiation
+  def do_negotiate(telopt)
+    res, arg = rfc1143_negotiate(telopt)
+    send_event(@state, telopt, res, arg)
+  end
+  
+  
+  # Process a subnegotiation buffer for a naws event
+  def subnegotiate_naws(buffer)
+    # Some clients, like Gnome-Mud can't even get this right. Grrr!
+    if buffer.nil? || buffer.empty? || buffer.size != 4
+      log_info("Bad NAWS negotiation: #{buffer}")
+      return nil
+    end
+    arr   = buffer.bytes.to_a
+    w     = (arr[0] << 8) + arr[1]
+    h     = (arr[2] << 8) + arr[3]
+    send_event(:naws, w, h)
+  end
+  
+
+  # Storage for environment values
+  class Environment 
+    attr_accessor :type
+    attr_accessor :value
+    
+    def initialize(type, value)
+      @type   = type
+      @value  = value
+    end
+  end
+
+
+  # process an ENVIRON/NEW-ENVIRON subnegotiation buffer
+  def subnegotiate_environ(buffer)
+    vars  = []
+    cmd   = ""
+    arr   = buffer.bytes.to_a
+    fb    = arr.first  
+    # first byte must be a valid command 
+    if fb != TELNET_ENVIRON_SEND && fb != TELNET_ENVIRON_IS && fb != TELNET_ENVIRON_INFO
+      log_error("telopt environment subneg command not valid")
+      return 0
+    end
+    
+    cmd << fb    
+    
+    if (buffer.size == 1) 
+      send_event(:environment, fb, vars)
+      return false
+    end
+        
+    # Second byte must be VAR or USERVAR, if present
+    sb = arr[1]
+    if sb != TELNET_ENVIRON_VAR && fb != TELNET_ENVIRON_USEVAR
+      log_error("telopt environment subneg missing variable type")
+      return false
+    end
+    
+    # ensure last byte is not an escape byte (makes parsing later easier) 
+    lb = arr.last
+    if lb == TELNET_ENVIRON_ESC
+      log_error("telopt environment subneg ends with ESC")
+      return false
+    end
+
+    var    = nil
+    index  = 1
+    escape = false
+    
+    arr.shift
+    
+    arr.each do | c | 
+      case c
+      when TELNET_ENVIRON_VAR
+      when TELNET_ENVIRON_VALUE
+      when TELNET_ENVIRON_USERVAR
+        if escape
+          escape = false
+          var.value << c
+        elsif var
+          vars << var
+          var = Environment.new(c, "")
+        else
+          var = Environment.new(c, "")        
+        end
+      when TELNET_ENVIRON_ESC
+        escape = true
+      else
+        var.value << c  
+      end # case
+    end # each
+    
+    send_event(:environment, fb, vars)    
+    return false
+  end
+
+
+
+# process an MSSP subnegotiation buffer
+def subnegotiate_mssp(buffer)
+  telnet_event_t ev;
+  struct telnet_environ_t *values;
+  char *var = 0;
+  char *c, *last, *out;
+  size_t i, count;
+  unsigned char next_type;
+  
+  if buffer.size < 1
+    return 0
+  end
+  
+  arr   = buffer.bytes.to_a
+  fb    = arr.first  
+  # first byte must be a valid command
+  if fb != TELNET_MSSSP_VAR
+    log_error("telopt MSSP subneg data not valid")
+    return false
+  end
+  
+  vars    = {}
+  var     = ""
+  val     = ""
+  mstate  = :var
+  while index <  arr.size
+    c     = arr[index]
+    case c
+    when TELNET_MSSP_VAR
+      mstate = :var
+      if mstate == :val
+        vars[var] = val
+        var = ""
+        val = ""
+      end      
+    when TELNET_MSSP_VAL
+      mstate = :val
+    else
+      if mstate == :var
+        var << c  
+      elsif mstate == :val
+        val << c  
+      end      
+    end # case
+    index += 1
+  end # while
+  
+  send_event(:mssp, vars)
+  return false
+end
+
+
+# parse ZMP command subnegotiation buffers 
+def subnegotiate_zmp(buffer)
+  args = []
+  arg  = ""
+  
+  buffer.each_byte do |b|  
+    if b == 0
+      args << arg
+      arg = ""
+    else
+      arg << byte
+    end
+  end
+  send_event(:zmp, vars)
+  return false
+end
+
+# parse TERMINAL-TYPE command subnegotiation buffers
+def subnegotiate_ttype(buffer)
+  # make sure request is not empty
+  if buffer.size == 0
+    log_error("Incomplete TERMINAL-TYPE request");
+    return 0
+  end
+  
+  arr   = buffer.bytes
+  fb    = arr.first
+  term  = nil 
+  
+  if fb == TELNET_TTYPE_IS
+    term = buffer[1, buffer.size]
+    send_event(:ttype_is, term)
+  elsif fb == TELNET_TTYPE_SEND
+    term = buffer[1, buffer.size]
+    send_event(:ttype_send, term)
+  else
+    log_error("TERMINAL-TYPE request has invalid type")
+    return false
+  end
+  return false
+end
+
+
+# process a subnegotiation buffer; returns true if the current buffer
+# must be aborted and reprocessed due to COMPRESS2 being activated
+
+def do_subnegotiate(buffer)
+  case @sb_telopt
+  when TELNET_TELOPT_COMPRESS2
+    # received COMPRESS2 begin marker, setup our zlib box and
+    # start handling the compressed stream if it's not already.
+    @compress = true
+    send_event(:compress, @compress)
+    return true
+  # specially handled subnegotiation telopt types
+  when TELNET_TELOPT_ZMP
+    return subnegotiate_zmp(buffer)
+  when TELNET_TELOPT_TTYPE
+    return subnegotiate_ttype(buffer)
+  when TELNET_TELOPT_ENVIRON  
+    return subnegotiate_environ(buffer)
+  when TELNET_TELOPT_NEW_ENVIRON
+    return subnegotiate_environ(buffer)
+  when TELNET_TELOPT_MSSP
+    return subnegotiate_mssp(buffer)
+  when TELNET_TELOPT_NAWS
+    return subnegotiate_naws(buffer)
+  else
+    send_event(:subnegotiate, @sb_telopt, buffer)
+    return false
+  end
+end
+
+
+  
+  def process_byte(byte) 
+    # p "process_byte, #{@state} #{byte}"
+    case @state
+    # regular data
+    when :data
+      if byte == TELNET_IAC
+        # receive buffered bytes as data and go to IAC state if it's notempty
+        send_event(:data, @buffer) unless @buffer.empty?
+        @buffer = ""
+        @state = :iac
+      else
+        @buffer << byte
+      end
+    # IAC received before
+    when :iac
+      case byte
+      # subnegotiation
+      when TELNET_SB
+        @state = :sb
+      # negotiation commands
+      when TELNET_WILL
+        @state = :will
+      when TELNET_WONT
+        @state = :wont
+      when TELNET_DO
+        @state = :do
+      when TELNET_DONT
+        @state = :dont
+      # IAC escaping 
+      when TELNET_IAC
+        @buffer << TELNET_IAC.chr
+        send_event(:data, @buffer) unless @buffer.empty?
+        @buffer = ""
+        @state = :data
+      # some other command
+      else
+        send_event(:iac, byte)
+        @state = :data
+      end
+
+    # negotiation received before
+    when :will, :wont, :do, :dont
+      do_negotiate(byte)
+      @state = :data
+    # subnegotiation started, determine option to subnegotiate
+    when :sb
+      @sb_telopt = byte
+      @state     = :sb_data
+    # subnegotiation data, buffer bytes until the end request 
+    when :sb_data
+      # IAC command in subnegotiation -- either IAC SE or IAC IAC
+      if (byte == TELNET_IAC)
+        @state = :sb_data_iac
+      elsif (@sb_telopt == TELNET_TELOPT_COMPRESS && byte == TELNET_WILL)
+        # MCCPv1 defined an invalid subnegotiation sequence (IAC SB 85 WILL SE) 
+        # to start compression. Catch and discard this case, only support 
+        # MMCPv2.
+        @state = data
+      else 
+        @buffer << byte
+      end
+
+    # IAC received inside a subnegotiation
+    when :sb_data_iac
+      case byte
+        # end subnegotiation
+        when TELNET_SE
+          @state = :data
+          # process subnegotiation
+          compress = do_subnegotiate(@buffer)
+          # if compression was negotiated, the rest of the stream is compressed
+          # and processing it requires decompressing it. Return true to signal 
+          # this.
+          @buffer = ""
+          return true if compress
+        # escaped IAC byte
+        when TELNET_IAC
+        # push IAC into buffer */
+          @buffer << byte
+          @state = :sb_data
+        # something else -- protocol error.  attempt to process
+        # content in subnegotiation buffer, then evaluate the
+        # given command as an IAC code.
+        else
+          log_error("Unexpected byte after IAC inside SB: %d", byte)
+          @state = :iac
+          # subnegotiate with the buffer anyway, even though it's an error
+          compress = do_subnegotiate(@buffer)
+          # if compression was negotiated, the rest of the stream is compressed
+          # and processing it requires decompressing it. Return true to signal 
+          # this.
+          @buffer = ""
+          return true if compress
+        end
+    when :data  
+      # buffer any other bytes
+      @buffer << byte
+    else 
+      # programing error, shouldn't happen
+      raise "Error in telet state machine!"
+    end
+    # return false to signal compression needn't start
+    return false
+  end
+  
+  def process_bytes(bytes)
+    # I have a feeling this way of handling strings isn't very efficient.. :p
+    arr = bytes.bytes.to_a
+    byte = arr.shift
+    while byte
+      compress = process_byte(byte)
+      if compress
+        # paper over this for a while... 
+        new_bytes = Zlib.inflate(arr.pack('c*')) rescue nil
+        if new_bytes
+          arr = new_bytes.bytes.to_a
+        end
+      end
+      byte = arr.shift    
+    end
+    send_event(:data, @buffer) unless @buffer.empty?
+    @buffer = ""
+  end
+  
+  # Call this when the server receives data from the client
+  def telnet_receive(data)
+    # the COMPRESS2 protocol seems to be half-duplex in that only 
+    # the server's data stream is compressed (unless maybe if the client
+    # is asked to also compress with a DO command ?)
+    process_bytes(data)
+  end
+  
+  # Send a bytes array (raw) to the client
+  def telnet_send_bytes(*bytes)
+    s     = bytes.pack('C*')
+    send_raw(s)
+  end
+  
+  # send an iac command 
+  def telnet_send_iac(cmd)
+    telnet_send_bytes(TELNET_IAC, cmd)
+  end
+
+  # send negotiation
+  def telnet_send_negotiate(cmd, telopt)
+    # get current option states
+    q = rfc1143_get(telopt)
+    unless q
+      rfc1143_set(telopt)
+      q = rfc1143_get(telopt)
+    end
+    
+    act, arg = nil, nil
+    case cmd
+      when TELNET_WILL
+        act, arg = q.send_will
+      when TELNET_WONT
+        act, arg = q.send_wont
+      when TELNET_DO
+        act, arg = q.send_do
+      when TELNET_DONT
+        act, arg = q.send_dont    
+    end
+        
+    return false unless act    
+    telnet_send_bytes(TELNET_IAC, act, telopt)
+  end
+        
+
+  # send non-command data (escapes IAC bytes)
+  def telnet_send(buffer)
+    send_escaped(buffer)
+  end
+  
+  # send subnegotiation header
+  def telnet_begin_sb(telopt)
+    telnet_send_bytes(TELNET_IAC, TELNET_SB, telopt)
+  end
+
+  # send subnegotiation ending
+  def telnet_end_sb()
+    telnet_send_bytes(TELNET_IAC, TELNET_SE)
+  end
+
+
+  # send complete subnegotiation
+  def telnet_subnegotiation(telopt, buffer = nil)
+    telnet_send_bytes(TELNET_IAC, TELNET_SB, telopt)
+    telnet_send(buffer) if buffer;
+    telnet_send_bytes(TELNET_IAC, TELNET_SE)
+  end
+  
+  # start compress2 compression
+  def telnet_begin_compress2() 
+    telnet_send_bytes(TELNET_IAC, TELNET_SB, TELNET_TELOPT_COMPRESS2, TELNET_IAC, TELNET_SE);
+    @compress = true
+  end
+  
+  # send formatted data
+  def telnet_raw_printf(fmt, *args)
+    buf   = sprintf(fmt, *args)
+    telnet_send(buf)
+  end
+
+  CRLF  = "\r\n"
+  CRNUL = "\r\0"
+  
+  # send formatted data with \r and \n translation in addition to IAC IAC 
+  def telnet_printf(fmt, *args)
+    buf   = sprintf(fmt, *args)
+    buf.gsub!("\r", CRNUL)
+    buf.gsub!("\n", CRLF)
+    telnet_send(buf)
+  end
+
+  # begin NEW-ENVIRON subnegotation
+  def telnet_begin_newenviron(cmd)
+    telnet_begin_sb(TELNET_TELOPT_NEW_ENVIRON)
+    telnet_send_bytes(cmd)
+  end
+  
+  # send a NEW-ENVIRON value
+  def telnet_newenviron_value(type, value)
+    telnet_send_bytes(type)
+    telnet_send(string)
+  end
+  
+  # send TERMINAL-TYPE SEND command
+  def telnet_ttype_send() 
+    telnet_send_bytes(TELNET_IAC, TELNET_SB, TELNET_TELOPT_TTYPE, TELNET_TTYPE_SEND, TELNET_IAC, TELNET_SE)
+  end  
+  
+  # send TERMINAL-TYPE IS command 
+  def telnet_ttype_is(ttype)
+    telnet_send_bytes(TELNET_IAC, TELNET_SB, TELNET_TELOPT_TTYPE, TELNET_TTYPE_IS)
+    telnet_send(ttype)
+  end
+  
+  # send MSSP data
+  def telnet_send_mssp(mssp)
+    buf = ""
+    mssp.each do | key, val| 
+      buf << TELNET_MSSP_VAR.chr
+      buf << key
+      buf << TELNET_MSSP_VAL.chr
+      buf << val      
+    end
+    telnet_subnegotiation(TELNET_TELOPT_MSSP, buf)
+  end
+
+end

+ 0 - 0
lib/telnet/codes.rb → lib/telnet/codes.cr


+ 0 - 0
lib/woe/account.rb → lib/woe/account.cr


+ 0 - 0
lib/woe/affects.rb → lib/woe/affects.cr


+ 0 - 0
lib/woe/ansi.rb → lib/woe/ansi.cr


+ 0 - 0
lib/woe/art.rb → lib/woe/art.cr


+ 0 - 0
lib/woe/being.rb → lib/woe/being.cr


+ 0 - 0
lib/woe/character.rb → lib/woe/character.cr


+ 0 - 0
lib/woe/client.rb → lib/woe/client.cr


+ 0 - 0
lib/woe/cserver.rb → lib/woe/cserver.cr


+ 0 - 0
lib/woe/equipment.rb → lib/woe/equipment.cr


+ 0 - 0
lib/woe/eserver.rb → lib/woe/eserver.cr


+ 0 - 0
lib/woe/exit.rb → lib/woe/exit.cr


+ 0 - 0
lib/woe/fserver.rb → lib/woe/fserver.cr


+ 0 - 0
lib/woe/gserver.rb → lib/woe/gserver.cr


+ 0 - 0
lib/woe/inventory.rb → lib/woe/inventory.cr


+ 0 - 0
lib/woe/item.rb → lib/woe/item.cr


+ 0 - 0
lib/woe/mobile.rb → lib/woe/mobile.cr


+ 0 - 0
lib/woe/profession.rb → lib/woe/profession.cr


+ 0 - 0
lib/woe/room.rb → lib/woe/room.cr


+ 0 - 0
lib/woe/server.rb → lib/woe/server.cr


+ 0 - 0
lib/woe/server_doesnt_work.rb → lib/woe/server_doesnt_work.cr


+ 0 - 0
lib/woe/settings.rb → lib/woe/settings.cr


+ 0 - 0
lib/woe/skill.rb → lib/woe/skill.cr


+ 0 - 0
lib/woe/technique.rb → lib/woe/technique.cr


+ 0 - 0
lib/woe/world.rb → lib/woe/world.cr


+ 0 - 0
lib/woe/zone.rb → lib/woe/zone.cr


+ 7 - 0
shard.yml

@@ -0,0 +1,7 @@
+name: woe
+version: 0.1.0
+
+authors:
+  -  <>
+
+license: MIT

+ 5 - 0
src/woe.cr

@@ -0,0 +1,5 @@
+require "./woe/*"
+
+module Woe
+  # TODO Put your code here
+end

+ 3 - 0
src/woe/version.cr

@@ -0,0 +1,3 @@
+module Woe
+  VERSION = "0.1.0"
+end