Browse Source

Switch from C+mruby to plain ruby with EventMachine. What I was programming on the C side was a square wheel reinvention of EventMachine anyway. I will have to port the Telnet handling, though.

Beoran 8 years ago
parent
commit
9b064a0174

+ 1 - 1
Tupfile

@@ -32,7 +32,7 @@ TEST_FILES = test/test_model.c
 MAIN_FILE  = src/main.c
 
 MRUBY_LIBS = -lmruby_core -lmruby
-WOE_LIBS   = $(MRUBY_LIBS) -lrt -lm
+WOE_LIBS   = $(MRUBY_LIBS) -lrt -lcrypt -lm
 
 LDFLAGS = -L /usr/local/lib $(WOE_LIBS)
 

+ 21 - 1
data/script/account.rb

@@ -3,11 +3,31 @@ script "serdes.rb"
 
 class Account
   include Serdes
-  
+
   serdes_reader :id
   serdes_reader :pass
   serdes_reader :algo
   
+  def inspect
+    "Account #{@id} #{@pass} #{algo}"
+  end
+  
+  def password=(pass)
+    @algo = "crypt"
+    @pass = crypt(pass)
+  end
+  
+  # Returns true if the password matches that of this account or false if not.
+  def challenge?(trypass) 
+    if algo == "plain"
+      return @pass == trypass
+    elsif algo == "crypt"
+      return crypt_challenge(trypass, @pass)
+    else
+      return false
+    end
+  end
+  
 end
 
 

+ 72 - 11
data/script/client.rb

@@ -1,19 +1,28 @@
 
 # Model and handle the clients of the server
 class Client 
-  attr_reader :id
-  attr_reader :buffer
-  attr_reader :in_lines
+  include Telnet
+  
+  attr_reader   :id
+  attr_reader   :buffer
+  attr_reader   :in_lines
+  attr_accessor :account
+  attr_accessor :character
+  attr_accessor :mode
   
   def initialize(id)
     @id       = id
     @buffer   = ""
     @in_lines = []
+    @account  = nil
+    @character= nil
+    @mode     = Mode::Setup.new(self)
   end
   
   def self.add(client_id)
     @clients ||= {}
     @clients[client_id] = Client.new(client_id)
+    return @clients[client_id]
   end
   
   def self.get(client_id)
@@ -25,8 +34,8 @@ class Client
     @clients[client_id] = nil
   end
   
-  def send(text) 
-    log "Client #{@id}, send #{text}"
+  def send_to_client(text)
+    log "Client #{@id}, send_to_client #{text}"
     Woe::Server.send_to_client(@id, text)
   end
   
@@ -36,7 +45,11 @@ class Client
   end
   
   def printf(fmt, *args)
-    text = fmt.format(*args) 
+    if args && !args.empty?
+      text = fmt.format(*args) 
+    else
+      text = fmt
+    end
     puts(text)
   end
 
@@ -45,26 +58,74 @@ class Client
     Woe::Server.raw_puts(@id, text)
   end
   
-  def raw_printf(fmt, *args)
-    text = fmt.format(*args) 
+  def raw_printf(fmt, *args)    
+    if args
+      text = fmt.format(*args) 
+    else
+      text = fmt
+    end
     puts(text)
   end
   
+  def negotiate(how, what)
+    log "Client #{@id} negotiate #{how} #{what}"
+    Woe::Server.negotiate(@id, how, what)
+  end
+  
+  def password_mode
+    self.negotiate(TELNET_WILL, TELNET_TELOPT_ECHO)
+  end
+  
+  def normal_mode
+    self.negotiate(TELNET_WONT, TELNET_TELOPT_ECHO)
+  end
+  
+  
+  def on_negotiate(how, opt)
+    if (opt == TELNET_TELOPT_COMPRESS2) 
+      if (how == TELNET_DO)
+        log "Beginning compress2 mode"
+        Woe::Server.begin_compress2(@id)
+      end
+    elsif (opt == TELNET_TELOPT_TELOPT_TTYPE) 
+      if (how == TELNET_WILL) || (opt == TELNET_DO)
+        
+      end
+    end
+  end
+  
+  
+  
+  def ask_type   
+    self.negotiate(TELNET_DO, TELNET_TELOPT_TTYPE
+  end
+  
+  
+  def on_start
+    ask_ttype
+    @mode.on_start
+  end
   
   def on_input(str)
     @buffer ||= ""
     @buffer << str
     if @buffer["\r\n"]
       command, rest = @buffer.split("\r\n", 1)
+      command.chomp!
       log "Client #{@id}, command #{command}"
-      if (command.strip == "quit") 
-        self.send("Bye bye!")
+      if (command.strip == "!quit") 
+        self.send_to_client("Bye bye!")
         Woe::Server.disconnect(@id)
         Client.remove(@id)
         return nil
+      elsif (command.strip == "!load") 
+        log "Reloading main script."
+        self.puts("Reloading main script.")
+        script "main.rb"
       else 
-        self.send("I don't know how to #{command}")
+        @mode.do_command(command);
       end
+      command = nil
       @buffer = rest
     end    
   end  

+ 68 - 26
data/script/main.rb

@@ -4,10 +4,20 @@ script "log.rb"
 log "Main mruby script loaded OK."
 p Signal.constants
 
+script "motd.rb"
 script "sitef.rb"
+script "account.rb"
+script "security.rb"
+script "mode.rb"
+script "mode/setup.rb"
+script "mode/login.rb"
+script "mode/normal.rb"
+script "mode/character.rb"
 script "client.rb"
 script "timer.rb"
-script "account.rb"
+
+
+
 
 # Return an array of symbols of constants of klass that match value
 def const_syms(klass, value)
@@ -37,9 +47,12 @@ def woe_on_battle_tick
 end
 
 def woe_on_weather_tick
- # p "weather"
+  # p "weather"
 end
 
+def woe_on_save_tick
+  # p "weather"
+end
 
 
 
@@ -49,10 +62,11 @@ def start_timers
     log "Timers already started."
   else
     log "Staring timer(s)..."
-    Timer.add("healing" , 30.0, 30.0) { woe_on_healing_tick }
-    Timer.add("motion"  ,  5.0,  5.0) { woe_on_motion_tick }
-    Timer.add("battle"  ,  1.0,  1.0) { woe_on_battle_tick }
-    Timer.add("weather" , 90.0, 90.0) { woe_on_weather_tick }
+    Timer.add("healing" , 30.0      , 30.0)       { woe_on_healing_tick }
+    Timer.add("motion"  ,  5.0      ,  5.0)       { woe_on_motion_tick  }
+    Timer.add("battle"  ,  1.0      ,  1.0)       { woe_on_battle_tick  }
+    Timer.add("weather" , 90.0      , 90.0)       { woe_on_weather_tick }
+    Timer.add("save"    , 15 * 60.0 , 15 * 60.0)  { woe_on_save_tick    }
     
     #@timer_id = Woe::Server.new_timer()
     #Woe::Server.set_timer(@timer_id, 1.0, 1.0);
@@ -62,7 +76,8 @@ end
 
 def woe_on_connect(client_id)
   p "Client #{client_id} connected"
-  Client.add(client_id)
+  client = Client.add(client_id)
+  client.on_start
 end
 
 def woe_on_disconnect(client_id)
@@ -70,73 +85,89 @@ def woe_on_disconnect(client_id)
   Client.remove(client_id)
 end
 
-def woe_on_input(client_id, buf)
-  p "Client #{client_id} input #{buf}"
+def woe_forward_to_client(client_id, method, *args)
   client = Client.get(client_id)
-  unless client
-    log "Unknown client #{client_id} in woe_on_input."
-    Woe::Server.disconnect(client_id)
-  else
+  if client
     p "Client #{client} #{client.id} ok."
-    client.on_input(buf)
-  end  
+    if client.respond_to?(method)
+      client.send(method, *args)
+    else
+      log "Client cannot handle #{method}."
+    end
+  else
+    log "Unknown client #{client_id} for #{method}."
+    Woe::Server.disconnect(client_id)
+  end
+end
+
+def woe_on_input(client_id, buf)
+  woe_forward_to_client(client_id, :on_input, buf)
 end
 
 def woe_on_negotiate(client_id, how, option) 
-  p "Client #{client_id} negotiating."
+  woe_forward_to_client(client_id, :on_negotiate, how, option)
 end
 
 def woe_on_subnegotiate(client_id, option, buffer) 
-  p "Client #{client_id} subnegotiating."
+  woe_forward_to_client(client_id, :on_subnegotiate, option, buffer)
 end
 
 def woe_on_iac(client_id, option, command) 
-  p "Client #{client_id} iac #{command}."
+  woe_forward_to_client(client_id, :on_iac, option, command)
 end
 
-
 def woe_on_ttype(client_id, cmd, name) 
-  p "Client #{client_id} ttype #{cmd} #{name}."
+  woe_forward_to_client(client_id, :on_ttype, cmd, name)
 end
 
 def woe_on_error(client_id, code, message) 
+  woe_forward_to_client(client_id, :on_error, code, message)
 end
 
 def woe_on_warning(client_id, code, message) 
+  woe_forward_to_client(client_id, :on_warning, code, message)
 end
 
-
 def woe_begin_compress(client_id, state) 
+  woe_forward_to_client(client_id, :on_compress, state)
 end
 
 
 def woe_begin_zmp(client_id, size) 
+  woe_forward_to_client(client_id, :on_begin_zmp, size)
 end
 
 def woe_zmp_arg(client_id, index, value)  
+  woe_forward_to_client(client_id, :on_zmp_arg, index, value)
 end
 
 def woe_finish_zmp(client_id, size) 
+  woe_forward_to_client(client_id, :on_finish_zmp, size)
 end
 
-
 def woe_begin_environ(client_id, size) 
+  woe_forward_to_client(client_id, :on_begin_environ, size)
 end
 
 def woe_environ_arg(client_id, index, type, key, value)  
+  woe_forward_to_client(client_id, :on_environ_arg, index, type, key, value)
 end
 
 def woe_finish_environ(client_id, size) 
+  woe_forward_to_client(client_id, :on_finish_environ, size)
 end
 
 
 def woe_begin_mssp(client_id, size) 
+  woe_forward_to_client(client_id, :on_begin_mssp, size)
 end
 
 def woe_mssp_arg(client_id, index, type, key, value)  
+  woe_forward_to_client(client_id, :on_mssp_arg, index, type, key, value)
 end
 
 def woe_finish_mssp(client_id, size) 
+  woe_forward_to_client(client_id, :on_finish_mssp, size)
 end
 
 
@@ -147,7 +178,7 @@ def woe_on_signal(signal)
     when 10 # SIGUSR1
       log "Reloading main script."
       script "main.rb"
-    when 28 
+    when 28 # SIGWINCH
       # ignore this signal
     else
       Woe::Server.quit 
@@ -196,9 +227,20 @@ if f
 end
 =end
 
-
-a = Account.new(:id => 'Dyon', :pass => 'noyd8pass')
+=begin
+a = Account.new(:id => 'Dyon', :pass => 'DN33Fbe/OGrM6', 
+  :algo => 'crypt'
+)
 p a.id
 a.save
 
-Account.
+d = Account.serdes_fetch('Dyon')
+p d
+Account.serdes_forget('Dyon')
+d = Account.serdes_fetch('Dyon')
+p d
+=end
+
+p crypt("noyd8pass")
+
+

+ 14 - 0
data/script/mode.rb

@@ -0,0 +1,14 @@
+
+# different modes of the client state
+module Mode
+  def do_command(str)
+    met = "on_#{@state}".to_sym
+    if self.respond_to?(met) 
+      self.send(met, str)
+    else
+      log "Unknown state #{@state}"
+      @state = :login
+    end
+  end
+end
+

+ 27 - 0
data/script/security.rb

@@ -0,0 +1,27 @@
+#
+# 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 Woe.crypt(pass, salt)
+end
+
+# Challenge crypt password trypass against the hash hash
+def crypt_challenge(trypass, hash) 
+  salt = hash[0, 2]
+  tryhash = Woe.crypt(trypass, salt)
+  p "cc", salt, tryhash, trypass
+  return tryhash == hash
+end

+ 43 - 26
data/script/serdes.rb

@@ -18,7 +18,7 @@ module Serdes
   module ClassMethods
     def serdes_add_to_fields(name, type = nil)
       @serdes_fields ||= []
-      info = { :name => name, :type => type}
+      info = { :name => name, :type => type }
       @serdes_fields << info
     end
     
@@ -48,44 +48,56 @@ module Serdes
       @serdes_loaded[obj.id] = obj
     end
     
-    def serdes_delete(id)
+    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(id)
-      full_name = serdes_dir_name(self) + '/' + serdes_file_name(self)
-      data  = Sitef.load_filename(full_name)
+      return nil unless id && !id.empty?
+      
+      full_name = Serdes.serdes_dir_name(self, id) + '/' + Serdes.serdes_file_name(self, id)
+      data, errors  = Sitef.load_filename(full_name)
+      unless data
+        log errors.join(", ")
+        return nil
+      end
+      
       eldat = data.select do |el|
-        el['id'] == id
+        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]
-        type||= String
+        value = eldat[name.to_s]
         
         typevalue = nil
         
         if type.respond_to?(:serdes_load)
           typevalue = type.serdes_load(value)
-        elsif Kernel.respond_to?(type)
-          typevalue = Kernel.send(type, value) rescue nil 
+        elsif type && Kernel.respond_to?(type.to_sym)
+          typevalue = Kernel.send(type.to_sym, value) rescue nil 
         else
           typevalue = value
         end
       
-        typedat[key] = typevalue
-        return self.new(typedat)
+        typedat[name] = typevalue
       end
       
       obj = self.new(typedat)
@@ -98,6 +110,10 @@ module Serdes
       return serdes_load(id)
     end
     
+    alias :fetch :serdes_fetch
+    alias :load  :serdes_load
+    alias :get   :serdes_get
+    
     def from_serdes(id)
       return serdes_fetch(id)
     end
@@ -106,31 +122,31 @@ module Serdes
       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_name(obj)
-    top = obj.class.to_s.gsub('::', '/').downcase
+  def self.serdes_dir_name(klass, id)
+    top = klass.to_s.gsub('::', '/').downcase
     top << '/' 
-    top << obj.id.to_s[0]
+    top << id.to_s[0]
     top << '/' 
-    top << obj.id.to_s    
+    top << id.to_s    
     return top
   end
   
-  def self.serdes_file_name(obj)
-    top = obj.id.to_s.dup    
+  def self.serdes_file_name(klass, id)
+    top = id.to_s.dup    
     top << '.'
-    top << obj.class.to_s.gsub('::', '.').downcase
+    top << klass.to_s.gsub('::', '.').downcase
     return top 
   end
 
   def serdes_data
     data = {}
-    self.class.serdes_fetch.each do |info|
+    self.class.serdes_fields.each do |info|
       name  = info[:name]
       type  = info[:type]
       type||= String
@@ -143,22 +159,23 @@ module Serdes
       end
       data[key]    = wrapvalue
     end
-    p "serdes_data", data, self.class.serdes_register
     return data
   end
   
   def save
-    Dir.mkdir_p Serdes.serdes_dir_name(self)
+    Dir.mkdir_p Serdes.serdes_dir_name(self.class, self.id)
     data = serdes_data
-    full_name = Serdes.serdes_dir_name(self) + '/' + Serdes.serdes_file_name(self)
+    full_name = Serdes.serdes_dir_name(self.class, self.id) + 
+               '/' + Serdes.serdes_file_name(self.class, self.id)
     Sitef.save_filename(full_name, [ data ] )
   end
   
   def initialize(fields = {}) 
-    fields.each |key, value| do
+    fields.each  do |key, value|
+      p "Setting #{key} #{value}"
       instance_variable_set("@#{key}", value)
     end
-    self.klass.serdes_register(self)
+    self.class.serdes_register(self)
   end
 
 end

+ 3 - 3
data/script/sitef.rb

@@ -109,13 +109,13 @@ module Sitef
   def self.save_filename(filename, records)
     results , errors = nil, nil
     file = File.open(filename, 'wt')
-    return nil, ["Could not open #{filename}"] unless file
+    return false, ["Could not open #{filename}"] unless file
     begin 
-      results, errors = save_file(file, records)
+      save_file(file, records)
     ensure
       file.close
     end
-    return results, errors
+    return true, []
   end
   
 end

+ 4 - 7
data/var/account/B/Beoran/Beoran.account

@@ -1,7 +1,4 @@
-name: Beoran
-algo: clear
-pass: memuma,27
-character[2]:	[
-character[0]:	@Berne
-character[1]:	@Kei
-]
+:name:Beoran
+:algo:plain
+:pass:hello
+--

+ 2 - 6
data/var/motd

@@ -1,6 +1,2 @@
-__        __         _                    ___   __   _____            _        
-\ \      / /__  _ __| | _____ _ __ ___   / _ \ / _| | ____|_ __ _   _| |_ __ _ 
- \ \ /\ / / _ \| '__| |/ / _ \ '__/ __| | | | | |_  |  _| | '__| | | | __/ _` |
-  \ V  V / (_) | |  |   <  __/ |  \__ \ | |_| |  _| | |___| |  | |_| | || (_| |
-   \_/\_/ \___/|_|  |_|\_\___|_|  |___/  \___/|_|   |_____|_|   \__,_|\__\__,_|
-                                                                               
+Workers Of Eruta
+

+ 2 - 0
include/client.h

@@ -8,6 +8,8 @@
 #define _BSD_SOURCE
 #endif
 
+#define _XOPEN_SOURCE      
+
 #include <sys/socket.h>
 #include <netinet/in.h>
 #include <arpa/inet.h>

+ 179 - 0
lib/monolog.rb

@@ -0,0 +1,179 @@
+
+# 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: %d: ", 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
+  
+  extend(self)
+ end
+ 
+ 
+ 
+ 

+ 739 - 0
lib/telnet.rb

@@ -0,0 +1,739 @@
+
+# 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 Telnet
+  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}"
+  end
+  
+  # Wait for input from the server 
+  def wait_for_input
+    return Fiber.yield
+  end
+  
+  # Called when client data should be filtered before being passed to the server
+  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)
+        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
+
+  # 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
+    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

+ 110 - 0
lib/telnet/codes.rb

@@ -0,0 +1,110 @@
+# This module contains the contants used for Telnet
+# Based on code by Jon A. Lambert,  under the Zlib license.
+ 
+module Telnet
+
+module Codes
+
+  IAC = 255  # Command  - RFC 854, 855, 1123, 1143
+
+  # 2 byte commands
+  WILL = 251  # Will do option
+  WONT = 252  # Wont do option
+  DO   = 253  # Do option
+  DONT = 254  # Dont do option
+
+
+  SB = 250  # Subnegotiation begin # IAC SB <option> <parameters> IAC SE
+  SE = 240  # Subnegotiation end
+
+  # 1 byte commands
+  GA    = 249  # Go Ahead
+  NOP   = 241  # No-op
+  BRK   = 243  # Break
+
+  # In RFC 854
+  AYT   = 246  # Are you there?
+  AO    = 245  # abort output
+  IP    = 244  # interrupt
+  EL    = 248  # erase current line
+  EC    = 247  # erase current character
+
+  DM    = 242  # data mark - sent to demarcate end of urgent commands
+
+  EOR   = 239 # end of record (transparent mode)
+  ABORT = 238 # Abort process
+  SUSP  = 237 # Suspend process
+  EOF   = 236 # End of file
+
+  # Options
+  BINARY         =   0 # Transmit Binary - RFC 856
+  ECHO           =   1 # Echo - RFC 857
+  RCP            =   2 # Reconnection
+  SGA            =   3 # Suppress Go Ahead - RFC 858
+  NAMS           =   4 # Approx Message Size Negotiation
+  STATUS         =   5 # Status - RFC 859
+  TM             =   6 # Timing Mark - RFC 860
+  RCTE           =   7 # Remote Controlled Trans and Echo - RFC 563, 726
+  NAOL           =   8 # Output Line Width
+  NAOP           =   9 # Output Page Size
+  NAOCRD         =  10 # Output Carriage-Return Disposition - RFC 652
+  NAOHTS         =  11 # Output Horizontal Tab Stops - RFC 653
+  NAOHTD         =  12 # Output Horizontal Tab Disposition - RFC 654
+  NAOFFD         =  13 # Output Formfeed Disposition - RFC 655
+  NAOVTS         =  14 # Output Vertical Tabstops - RFC 656
+  NAOVTD         =  15 # Output Vertical Tab Disposition - RFC 657
+  NAOLFD         =  16 # Output Linefeed Disposition - RFC 658
+  XASCII         =  17 # Extended ASCII - RFC 698
+  LOGOUT         =  18 # Logout - RFC 727
+  BM             =  19 # Byte Macro - RFC 735
+  DET            =  20 # Data Entry Terminal - RFC 732, 1043
+  SUPDUP         =  21 # SUPDUP - RFC 734, 736
+  SUPDUPOUTPUT   =  22 # SUPDUP Output - RFC 749
+  SNDLOC         =  23 # Send Location - RFC 779
+  TTYPE          =  24 # Terminal Type - RFC 1091
+  EOREC          =  25 # End of Record - RFC 885
+  TUID           =  26 # TACACS User Identification - RFC 927
+  OUTMRK         =  27 # Output Marking - RFC 933
+  TTYLOC         =  28 # Terminal Location Number - RFC 946
+  REGIME3270     =  29 # Telnet 3270 Regime - RFC 1041
+  X3PAD          =  30 # X.3 PAD - RFC 1053
+  NAWS           =  31 # Negotiate About Window Size - RFC 1073
+  TSPEED         =  32 # Terminal Speed - RFC 1079
+  LFLOW          =  33 # Remote Flow Control - RFC 1372
+  LINEMODE       =  34 # Linemode - RFC 1184
+  XDISPLOC       =  35 # X Display Location - RFC 1096
+  ENVIRON        =  36 # Environment Option - RFC 1408
+  AUTHENTICATION =  37 # Authentication Option - RFC 1416, 2941, 2942, 2943, 2951
+  ENCRYPT        =  38 # Encryption Option - RFC 2946
+  NEW_ENVIRON    =  39 # New Environment Option - RFC 1572
+  TN3270         =  40 # TN3270 Terminal Entry - RFC 2355
+  XAUTH          =  41 # XAUTH
+  CHARSET        =  42 # Charset option - RFC 2066
+  RSP            =  43 # Remote Serial Port
+  CPCO           =  44 # COM port Control Option - RFC 2217
+  SUPLECHO       =  45 # Suppress Local Echo
+  TLS            =  46 # Telnet Start TLS
+  KERMIT         =  47 # Kermit tranfer Option - RFC 2840
+  SENDURL        =  48 # Send URL
+  FORWARDX       =  49 # Forward X
+  PLOGON         = 138 # Telnet Pragma Logon
+  SSPI           = 139 # Telnet SSPI Logon
+  PHEARTBEAT     = 140 # Telnat Pragma Heartbeat
+  EXOPL          = 255 # Extended-Options-List - RFC 861
+
+  MSDP     = 69  # Mud Server Data Protocol
+  MSSP     = 70  # MUD Server Status Protocol
+  COMPRESS =  85 # MCCP 1 support (broken deprecated)
+  COMPRESS2 = 86 # MCCP 2 support
+  MSP  = 90 # MSP  support
+  MXP  = 91 # MUD eXtension Protocol (MXP)
+  MSP2 = 92 # MSP2 support
+    MUSIC = 0
+    SOUND = 1
+
+  ZMP = 93 # ZMP support
+  AARD = 102 # Aardwolf client protocol (deprecated?)
+  MULTIPLEX = 112 # Crystal client telnet multiplex 
+  ATCP = 200 # Achaea telnet client protocol
+  GMCP = 201 # GMCP/ATCP2 client protocol
+end

+ 37 - 0
lib/woe/account.rb

@@ -0,0 +1,37 @@
+script "serdes.rb"
+
+
+class Account
+  include Serdes
+
+  serdes_reader :id
+  serdes_reader :pass
+  serdes_reader :algo
+  
+  def inspect
+    "Account #{@id} #{@pass} #{algo}"
+  end
+  
+  def password=(pass)
+    @algo = "crypt"
+    @pass = crypt(pass)
+  end
+  
+  # Returns true if the password matches that of this account or false if not.
+  def challenge?(trypass) 
+    if algo == "plain"
+      return @pass == trypass
+    elsif algo == "crypt"
+      return crypt_challenge(trypass, @pass)
+    else
+      return false
+    end
+  end
+  
+end
+
+
+
+
+
+

+ 0 - 0
lib/woe/ansi.rb


+ 165 - 0
lib/woe/client.rb

@@ -0,0 +1,165 @@
+require 'eventmachine'
+require 'tempfile'
+require 'fiber'
+
+
+module Woe
+
+class Minifilter
+
+  def initialize(give)
+    @give   = give
+    @fiber  = nil
+  end 
+  
+  def wait_for_input(val=nil)
+    return Fiber.yield(val)
+  end
+  
+  def filter_input(line)
+    if line =~ /2/
+      return (line + line)
+    end
+    
+    if line =~ /4/
+      res = wait_for_input
+      return (res * 4)      
+    end
+    
+    if line =~ /0/
+      return (nil)      
+    end
+    
+    return line
+  end
+    
+end
+
+class Client < EventMachine::Connection
+  attr_accessor :id
+  attr_accessor :server
+  
+  def initialize(*args)    
+    super(*args)
+    @id         = nil
+    @server     = nil
+    @connected  = false
+    @port       = nil
+    @ip         = nil
+    @fiber      = nil
+    @account    = nil
+    @filter     = ::Woe::Minifilter.new(self)
+  end
+  
+  def post_init()
+    send_data("Welcome!\n")
+    pn          = self.get_peername
+    @port, @ip  = Socket.unpack_sockaddr_in(pn)
+    send_data("You are connecting from #{@ip}:#{@port}\n")
+    @connected  = true
+    self.send_data("Login:")
+  end
+      
+  
+
+    
+  def save
+    self.send_data("Saving...")
+    
+    do_save = proc do 
+      begin
+        f = Tempfile.new('random')
+        sleep 3
+        f.write("I'm saving data.")
+      ensure 
+        f.close
+      end
+    end
+    
+    on_save = proc do
+      self.send_data("Saved.")
+    end
+    
+    EM.defer(do_save, on_save)    
+  end
+  
+  
+  # Basically, this method yields the fiber, and will return
+  # with the input that will cme later when the fiber is resumed, normally
+  # when more input becomes available from the client.
+  # The 
+  def wait_for_input
+    data = Fiber.yield
+    # the filters MUST be aplied here, since then it can also be 
+    # fake-syncronous and use Fiber.yield to wait for additional input if 
+    # needed 
+    line = @filter.filter_input(data)
+    return line
+  end
+  
+  def try
+    self.send_data("\nOK, let's try. What do you say?:")
+    try = wait_for_input
+    self.send_data("\nOK, nice try #{try}.\n")
+  end
+    
+  # Fake synchronous handing of input  
+  def handle_input()
+    @login    = wait_for_input
+    self.send_data("\nPassword for #{@login}:")
+    @password = wait_for_input
+    self.send_data("\nOK #{@password}, switching to command mode.\n")
+      
+    while @connected
+      line = wait_for_input
+      # If the user says 'quit', disconnect them
+      if line =~ /^\/quit/
+        @connected = false
+        close_connection_after_writing
+      # Shut down the server if we hear 'shutdown'
+      elsif line =~ /^\/reload/
+        @server.reload
+      elsif line =~ /^\/shutdown/
+        @connected = false
+        @server.stop
+      elsif line =~ /^\/save/      
+        self.save
+      elsif line =~ /^\/try/      
+        self.try          
+      else
+        @server.broadcast("Client #{id} says #{line}")
+      end
+    end
+  end  
+    
+  def receive_data(data)
+    # Ignore any input if already requested disconnection
+    return unless @connected
+    # 
+    if @fiber
+      @fiber.resume(data)
+    else      
+      # set up a fiber to handle the input
+      # Like that, the handle_input can be programmed in a fake-syncronous way
+      @fiber = Fiber.new do      
+        handle_input()
+      end
+      # Must resume twice becaus of the way handle_input works
+      @fiber.resume()
+      @fiber.resume(data)
+    end    
+  end
+  
+  
+  
+  
+  def unbind
+    $stderr.puts("Client #{id} has left")
+    @server.disconnect(@id)
+    
+  end
+end
+
+end
+
+

+ 93 - 0
lib/woe/gserver.rb

@@ -0,0 +1,93 @@
+require 'gserver'
+
+
+module Woe
+class Client
+  attr_reader :id
+  attr_reader :io
+  
+  def initialize(server, id, io)
+    @server = server
+    @id     = id
+    @io     = io
+    @busy   = true
+  end
+  
+  
+  def on_input(line)
+    # If the user says 'quit', disconnect them
+    if line =~ /^\/quit/
+      @busy = false
+    # Shut down the server if we hear 'shutdown'
+    elsif line =~ /^\/shutdown/
+      @server.stop
+    else
+      @server.broadcast("Client #{id} says #{line}")
+    end
+  end
+    
+  def serve_once
+      if IO.select([io], nil, nil, 0)
+        # If so, retrieve the data and process it..
+        line = io.gets
+        on_input(line)
+      else
+        
+      end
+  end
+  
+  def serve
+    while @busy
+      serve_once
+    end
+  end
+end
+
+
+class Server < GServer
+  def initialize(*args)
+    super(*args)
+    self.audit          = true
+    # increase the connection limit
+    @maxConnections     = 400 
+    # Keep an overall record of the client IDs allocated
+    # and the lines of chat
+    @client_id = 0
+    @clients   = []
+  end
+  
+  
+  
+  def serve(io)
+    # Increment the client ID so each client gets a unique ID
+    @client_id += 1
+    client      = Client.new(self, @client_id, io)
+    @clients << client 
+    client.io.puts("Welcome, client nr #{client.id}!")
+    client.serve
+  end
+  
+  def broadcast(msg)
+    p msg
+    @clients.each do |client|
+      client.io.puts(msg)
+    end
+  end
+  
+
+  def self.run(port=7000)
+    server = Woe::Server.new(port)
+    server.start
+    server.join
+  end
+    
+end
+
+
+
+end
+
+Woe::Server.run
+
+
+

+ 0 - 0
lib/woe/log.rb


+ 182 - 0
lib/woe/serdes.rb

@@ -0,0 +1,182 @@
+
+class Dir
+  def self.mkdir_p(name)
+    sub   = ""
+    parts = name.split('/').reject { |e| e.empty? }
+    parts.each do | part |
+      sub <<  "/#{part}"
+      mkdir sub
+    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)
+      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(id)
+      return nil unless id && !id.empty?
+      
+      full_name = Serdes.serdes_dir_name(self, id) + '/' + Serdes.serdes_file_name(self, id)
+      data, errors  = Sitef.load_filename(full_name)
+      unless data
+        log 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.respond_to?(:serdes_load)
+          typevalue = type.serdes_load(value)
+        elsif type && Kernel.respond_to?(type.to_sym)
+          typevalue = Kernel.send(type.to_sym, value) rescue nil 
+        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(id)
+    end
+    
+    alias :fetch :serdes_fetch
+    alias :load  :serdes_load
+    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_name(klass, id)
+    top = klass.to_s.gsub('::', '/').downcase
+    top << '/' 
+    top << id.to_s[0]
+    top << '/' 
+    top << id.to_s    
+    return top
+  end
+  
+  def self.serdes_file_name(klass, id)
+    top = id.to_s.dup    
+    top << '.'
+    top << klass.to_s.gsub('::', '.').downcase
+    return top 
+  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
+    Dir.mkdir_p Serdes.serdes_dir_name(self.class, self.id)
+    data = serdes_data
+    full_name = Serdes.serdes_dir_name(self.class, self.id) + 
+               '/' + Serdes.serdes_file_name(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
+

+ 82 - 0
lib/woe/server.rb

@@ -0,0 +1,82 @@
+require 'eventmachine'
+require 'tempfile'
+require 'fiber'
+
+
+module Woe
+  class Server 
+    def initialize(port =7000)
+      @port      = port
+      # Keep an overall record of the client IDs allocated
+      # and the lines of chat
+      @client_id = 0
+      @clients   = {}
+      @tick_id   = 0
+      @fiber     = nil
+    end
+    
+    def start()    
+      @signature = EventMachine.start_server("0.0.0.0", @port, Client) do |client|
+        @client_id          += 1
+        client.id            = @client_id
+        client.server        = self   
+        @clients[@client_id] = client
+      end
+      EventMachine.add_periodic_timer(1) do 
+        @tick_id            += 1
+        # self.broadcast("Tick tock #{@tick_id}\n")
+      end  
+    end
+    
+    def run
+      EventMachine.run do       
+        self.start
+      end  
+    end
+    
+    
+    def disconnect(id)
+      @clients.delete(id)
+    end
+    
+    def clients_stopped?    
+    end
+    
+    def reload
+      broadcast("Reloading\n")
+      begin 
+        load 'lib/woe/server.rb'
+        broadcast("Reloaded\n")
+      rescue Exception => ex
+        broadcast("Exception #{ex}: #{ex.backtrace.join("\n")}!\n")
+      end
+    end
+    
+    def stop
+      EventMachine.stop_server(@signature)
+      EventMachine.add_timer(1) { EventMachine.stop }
+    end
+    
+   
+    def broadcast(msg)
+      @clients.each do |id, client|
+        client.send_data(msg)
+      end
+    end
+    
+
+    def self.run(port=7000)    
+      server = Woe::Server.new(port)
+      server.run
+    end
+      
+  end
+end
+
+
+
+
+
+
+
+

+ 122 - 0
lib/woe/sitef.rb

@@ -0,0 +1,122 @@
+
+# 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, une 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)
+      # XXX does eof? even work???
+      break if line.nil?
+      next if line.empty? 
+      # new record
+      if line[0,2] == '--' 
+        # Store last key used if any.
+        if key
+          record[key.downcase] = 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.downcase] = value.chomp
+      key = value = nil
+      elsif line[0, 2] == '..'
+      # end of multiline value 
+      record[key.downcase] = 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]
+      value = ""
+      elsif key
+          # 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.downcase] = value.chomp
+    end  
+    # store last record 
+    results << record unless record.empty?
+    return results, errors
+  end  
+  
+  def self.load_filename(filename)
+    results , errors, warnings = nil, nil, nil;
+    file = File.open(filename, 'rt')
+    return nil, ["Could not open #{filename}"] unless file
+    begin 
+      results, errors = parse_file(file)
+    ensure
+      file.close
+    end
+    return results, errors
+  end
+  
+  def self.save_field(file, key, value)
+    sval = value.to_s
+    if sval["\n"]
+      file.puts(".#{key}\n")
+      file.puts(sval)
+      file.puts("\n..\n")
+    else
+      file.puts(":#{key}:#{sval}\n")
+    end
+  end
+  
+  def self.save_record(file, record)
+    record.each do | key, value |
+      save_field(file, key, value)
+    end
+  end
+
+  def self.save_file(file, records)
+    records.each do | record |
+      save_record(file, record)
+      file.puts("--\n")
+    end
+  end
+  
+  def self.save_filename(filename, records)
+    results , errors = nil, nil
+    file = File.open(filename, 'wt')
+    return false, ["Could not open #{filename}"] unless file
+    begin 
+      save_file(file, records)
+    ensure
+      file.close
+    end
+    return true, []
+  end
+  
+end
+

+ 1 - 0
script

@@ -0,0 +1 @@
+data/script

+ 19 - 4
src/toruby.c

@@ -4,6 +4,8 @@
 * Look at the tr_*.c files.
 * */
 
+#define _XOPEN_SOURCE 700     
+
 #include "toruby.h"
 #include "tr_macro.h"
 #include "monolog.h"
@@ -13,6 +15,7 @@
 #include "libtelnet.h"
 #include "tr_file.h"
 
+#include <unistd.h>
 #include <signal.h>
 #include <mruby/hash.h>
 #include <mruby/class.h>
@@ -319,6 +322,17 @@ static mrb_value tr_server_get_timer(mrb_state * mrb, mrb_value self) {
 }
 
 
+static mrb_value tr_crypt(mrb_state * mrb, mrb_value self) {
+  mrb_value res;
+  char * pass = NULL;
+  char * salt = NULL;
+  char * hash = NULL;
+  mrb_get_args(mrb, "zz", &pass, &salt);
+  hash = crypt(pass, salt);
+  return mrb_str_new(mrb, hash, 13);
+}
+                            
+
 
 /* Initializes the functionality that Eruta exposes to Ruby. */
 int tr_init(mrb_state * mrb) {
@@ -339,10 +353,11 @@ int tr_init(mrb_state * mrb) {
   krn = mrb_module_get(mrb, "Kernel");
   if(!krn) return -1;
   
-  TR_CLASS_METHOD_NOARG(mrb, woe, "quit"  , tr_server_done);
-  TR_CLASS_METHOD_NOARG(mrb, srv, "quit"  , tr_server_done);
-  TR_CLASS_METHOD_ARGC(mrb, srv, "send_to_client"  , tr_send_to_client, 2);
-  TR_CLASS_METHOD_NOARG(mrb, srv, "disconnect"  , tr_disconnect_client);
+  TR_CLASS_METHOD_ARGC(mrb  , woe, "crypt"  , tr_crypt, 2);
+  TR_CLASS_METHOD_NOARG(mrb , woe,  "quit"  , tr_server_done);
+  TR_CLASS_METHOD_NOARG(mrb , srv, "quit"  , tr_server_done);
+  TR_CLASS_METHOD_ARGC(mrb  , srv, "send_to_client"  , tr_send_to_client, 2);
+  TR_CLASS_METHOD_NOARG(mrb , srv, "disconnect"  , tr_disconnect_client);
   
   TR_CLASS_METHOD_ARGC(mrb, srv, "iac"  , tr_server_iac, 2);
   TR_CLASS_METHOD_ARGC(mrb, srv, "negotiate"      , tr_server_negotiate     , 3);

+ 80 - 14
src/tr_file.c

@@ -39,20 +39,17 @@ static void tr_file_free(mrb_state *mrb, void *ptr) {
   mrb_free(mrb, file);
 }
 
-static tr_file * file_open(mrb_state * mrb, char * filename, char * mode) 
+static FILE * file_fopen(mrb_state * mrb, char * filename, char * mode) 
 {
   struct woe_config * cfg;
   struct woesb buf = { 0 };
-  tr_file * me = NULL;
+  FILE * me = NULL;
   if (!mrb) return NULL;
   cfg = MRB_WOE_CONFIG(mrb);
   if (!cfg) return NULL;
   
-  me  = mrb_malloc(mrb, sizeof(struct tr_file));
-  if (!me) return NULL;
   if (!woesb_new_join(&buf, cfg->data_dir, "/var/", filename, NULL)) {
     LOG_ERROR("Cannot allocate space for file name.\n");
-    mrb_free(mrb, me);
     return NULL;
   }
   
@@ -63,18 +60,37 @@ static tr_file * file_open(mrb_state * mrb, char * filename, char * mode)
     return NULL;
   }
 
-  me->file = fopen(buf.text, mode);
-  if (!me->file) {
+  me = fopen(buf.text, mode);
+  if (!me) {
     LOG_ERROR("Cannot open file %s.\n", filename);
-    mrb_free(mrb, me);
     woesb_free(&buf);
     return NULL;
   }
-  me->cfg = cfg; 
+  
   woesb_free(&buf);
   return me;
 }
 
+
+static tr_file * file_open(mrb_state * mrb, char * filename, char * mode) 
+{
+  struct woe_config * cfg;
+  tr_file * me = NULL;
+  if (!mrb) return NULL;
+  cfg = MRB_WOE_CONFIG(mrb);
+  if (!cfg) return NULL;
+  
+  me  = mrb_malloc(mrb, sizeof(struct tr_file));
+  if (!me) return NULL;
+  me->file = file_fopen(mrb, filename, mode);
+  if (!me->file) {
+    mrb_free(mrb, me);
+    return NULL;
+  }
+  me->cfg = cfg; 
+  return me;
+}
+
 int woe_mkdir(struct woe_config * cfg, char * filename) {
   int res;
   DIR * dir;
@@ -199,27 +215,76 @@ static mrb_value tr_file_write(mrb_state * mrb, mrb_value self) {
 
 static mrb_value tr_file_read(mrb_state * mrb, mrb_value self) {
   mrb_int res, size;
-  tr_file * file; 
+  tr_file * file;
+  char * mem; 
   mrb_value buf;
   
+  
   file = tr_file_unwrap(mrb, self);
   mrb_get_args(mrb, "i", &size);
-  buf = mrb_str_buf_new(mrb, size);
- 
-  res = fread(RSTRING(buf), size, 1, file->file);
+  mem = calloc(size, 1);
+  res = fread(mem, 1, size, file->file);
   if (res > 0) {
-    mrb_str_resize(mrb, buf, res); 
+    buf = mrb_str_new(mrb, mem, size);
+    free(mem);
     return buf;
   } 
   
   if (res == 0) {
+    free(mem);
     return mrb_nil_value();
   }
   
   // if (res < 0)
+  free(mem);
   LOG_ERROR("Failed to read from file.\n");
   return mrb_nil_value();
 }   
+
+
+static mrb_value tr_file_readall(mrb_state * mrb, mrb_value self) {
+  #define READALL_BUFSIZE 1024
+  mrb_int res = 0, size = 0;
+  FILE * file;
+  char * mem = NULL, * aid = NULL; 
+  char * filename = NULL;
+  mrb_value buf;
+  (void) self;
+  
+  
+  mrb_get_args(mrb, "z", &filename);
+  if (!filename) {
+      return mrb_nil_value();
+  }
+  
+  file = file_fopen(mrb, filename, "rb");
+  
+  if (!file) { 
+    return mrb_nil_value();
+  }
+  
+  while (!feof(file)) {
+    size += READALL_BUFSIZE;
+    aid   = realloc(mem, size);
+    if (!aid) { 
+      buf = mrb_nil_value();
+      goto done;
+    }
+    mem = aid;
+    res   = fread(mem + size - READALL_BUFSIZE, 1, READALL_BUFSIZE, file);
+    
+    if (res < READALL_BUFSIZE) {
+      size = size - READALL_BUFSIZE + res;
+      break;
+    }
+  }
+  buf = mrb_str_new(mrb, mem, size);
+  
+  done:  
+    free(mem);
+    fclose(file);
+    return buf;
+}   
  
 
 static mrb_value tr_file_puts(mrb_state * mrb, mrb_value self) { 
@@ -312,6 +377,7 @@ int tr_init_file(mrb_state * mrb) {
   fil = mrb_define_class(mrb, "File"    , mrb_class_get(mrb, "Object"));
   dir = mrb_define_class(mrb, "Dir"     , mrb_class_get(mrb, "Object"));
 
+  TR_CLASS_METHOD_ARGC(mrb, fil, "read" , tr_file_readall, 1);
   TR_CLASS_METHOD_ARGC(mrb, fil, "open" , tr_file_open, 2);
   TR_CLASS_METHOD_ARGC(mrb, fil, "link" , tr_file_link, 2);
   TR_CLASS_METHOD_ARGC(mrb, dir, "mkdir", tr_dir_mkdir, 1);

+ 36 - 0
test/test_monolog.rb

@@ -0,0 +1,36 @@
+require 'atto'
+include Atto::Test
+
+require_relative '../lib/monolog' 
+
+LOG_NAME = '/tmp/monolog_test.log'
+
+assert { Monolog } 
+assert { Monolog.setup }
+assert { Monolog.setup_all(LOG_NAME) }
+
+assert { Monolog.get_log }
+
+assert do
+  lg = Monolog.get_log.loggers 
+  !(lg.empty?)
+end
+
+
+assert do
+  Monolog.log_info("bazz") 
+  sleep 1
+  res = File.read(LOG_NAME)
+  res =~ /bazz/ && res =~ /INFO/
+end
+
+assert "Debug evel is not logged by default after setup_all" do
+  Monolog.log_debug("frotz")
+  sleep 1
+  res = File.read(LOG_NAME)
+  res !~ /frotz/
+end
+
+
+assert { Monolog.close }
+

+ 54 - 29
woe.geany

@@ -23,37 +23,62 @@ long_line_behaviour=1
 long_line_column=80
 
 [files]
-current_page=16
+current_page=38
 FILE_NAME_0=32;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fsrc%2Fmain.c;0;2
 FILE_NAME_1=1652;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fsrc%2Fcallrb.c;0;2
-FILE_NAME_2=9463;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fsrc%2Ftoruby.c;0;2
-FILE_NAME_3=12135;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fsrc%2Fserver.c;0;2
-FILE_NAME_4=4816;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fsrc%2Flibtelnet.c;0;2
-FILE_NAME_5=2640;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fsrc%2Fclient.c;0;2
-FILE_NAME_6=158;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fsrc%2Fconfig.c;0;2
-FILE_NAME_7=1302;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Finclude%2Fclient.h;0;2
-FILE_NAME_8=240;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Finclude%2Fconfig.h;0;2
-FILE_NAME_9=3599;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Finclude%2Fesh.h;0;2
-FILE_NAME_10=3217;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fsrc%2Fesh.c;0;2
-FILE_NAME_11=264;None;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2FTupfile;0;2
-FILE_NAME_12=1810;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Finclude%2Frh.h;0;2
-FILE_NAME_13=7445;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Finclude%2Ftr_macro.h;0;2
-FILE_NAME_14=1250;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fsrc%2Frh.c;0;2
-FILE_NAME_15=2107;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fdata%2Fscript%2Fmain.rb;0;2
-FILE_NAME_16=842;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fdata%2Fscript%2Fserdes.rb;0;2
-FILE_NAME_17=137;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fdata%2Fscript%2Flog.rb;0;2
-FILE_NAME_18=578;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fdata%2Fscript%2Fclient.rb;0;2
-FILE_NAME_19=867;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Finclude%2Ftimer.h;0;2
-FILE_NAME_20=2864;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fsrc%2Ftimer.c;0;2
-FILE_NAME_21=60;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Finclude%2Fmem.h;0;2
-FILE_NAME_22=426;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Finclude%2Fstate.h;0;2
-FILE_NAME_23=61;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fsrc%2Fmonolog.c;0;2
-FILE_NAME_24=2307;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Finclude%2Flibtelnet.h;0;2
-FILE_NAME_25=1774;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Finclude%2Fmonolog.h;0;2
-FILE_NAME_26=235;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Finclude%2Fdynar.h;0;2
-FILE_NAME_27=103;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fsrc%2Fmem.c;0;2
-FILE_NAME_28=5343;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fsrc%2Fdynar.c;0;2
-FILE_NAME_29=2919;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Finclude%2Fserver.h;0;2
+FILE_NAME_2=10930;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fsrc%2Ftoruby.c;0;2
+FILE_NAME_3=1133;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fsrc%2Ftr_file.c;0;2
+FILE_NAME_4=11178;C;0;EUTF-8;0;1;0;%2Fusr%2Flocal%2Finclude%2Fmruby.h;0;2
+FILE_NAME_5=15634;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fsrc%2Fserver.c;0;2
+FILE_NAME_6=4816;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fsrc%2Flibtelnet.c;0;2
+FILE_NAME_7=2640;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fsrc%2Fclient.c;0;2
+FILE_NAME_8=8011;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Flibtelnet%2Futil%2Ftelnet-chatd.c;0;2
+FILE_NAME_9=158;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fsrc%2Fconfig.c;0;2
+FILE_NAME_10=154;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Finclude%2Fclient.h;0;2
+FILE_NAME_11=240;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Finclude%2Fconfig.h;0;2
+FILE_NAME_12=3599;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Finclude%2Fesh.h;0;2
+FILE_NAME_13=3217;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fsrc%2Fesh.c;0;2
+FILE_NAME_14=697;None;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2FTupfile;0;2
+FILE_NAME_15=1810;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Finclude%2Frh.h;0;2
+FILE_NAME_16=7445;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Finclude%2Ftr_macro.h;0;2
+FILE_NAME_17=1250;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fsrc%2Frh.c;0;2
+FILE_NAME_18=235;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fdata%2Fscript%2Fmain.rb;0;2
+FILE_NAME_19=602;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fdata%2Fscript%2Fsecurity.rb;0;2
+FILE_NAME_20=1315;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fdata%2Fscript%2Fserdes.rb;0;2
+FILE_NAME_21=521;None;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fdata%2Fvar%2Ftry.sitef;0;2
+FILE_NAME_22=143;Markdown;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fureqma%2FREADME.md;0;2
+FILE_NAME_23=341;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fureqma%2Fureqma;0;2
+FILE_NAME_24=348;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fdata%2Fscript%2Faccount.rb;0;2
+FILE_NAME_25=3275;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fdata%2Fscript%2Fsitef.rb;0;2
+FILE_NAME_26=152;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fdata%2Fscript%2Fmotd.rb;0;2
+FILE_NAME_27=35;None;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fdata%2Fvar%2Faccount%2FD%2FDyon%2FDyon.account;0;2
+FILE_NAME_28=40;None;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fdata%2Fvar%2Faccount%2FB%2FBeoran%2FBeoran.account;0;2
+FILE_NAME_29=29;None;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fdata%2Fvar%2Faccount%2Fa%2Faxl%2Faxl.account;0;2
+FILE_NAME_30=18;None;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fdata%2Fvar%2Fmotd;0;2
+FILE_NAME_31=0;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fdata%2Fscript%2Fcharacter.rb;0;2
+FILE_NAME_32=137;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fdata%2Fscript%2Flog.rb;0;2
+FILE_NAME_33=382;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fdata%2Fscript%2Fclient.rb;0;2
+FILE_NAME_34=245;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fdata%2Fscript%2Fmode.rb;0;2
+FILE_NAME_35=414;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fdata%2Fscript%2Fmode%2Fcharacter.rb;0;2
+FILE_NAME_36=669;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fdata%2Fscript%2Fmode%2Fsetup.rb;0;2
+FILE_NAME_37=6413;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Farch%2Fdl%2Fmud%2Frelease%2Ftmud-3.0.0%2Ftclient.rb;0;2
+FILE_NAME_38=2622;YAML;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Farch%2Fdl%2Fmud%2Frelease%2Ftmud-3.0.0%2Fconfig.yaml;0;2
+FILE_NAME_39=2069;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fdata%2Fscript%2Fmode%2Flogin.rb;0;2
+FILE_NAME_40=0;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fdata%2Fscript%2Fmode%2Fnormal.rb;0;2
+FILE_NAME_41=3438;Sh;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fwork%2Febp2git%2Febp_make_gitignore;0;2
+FILE_NAME_42=643;Sh;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fwork%2Febp2git%2Febp_make_all_gitignores;0;2
+FILE_NAME_43=1452;None;0;EUTF-8;0;1;0;%2Fusers%2Fnmbs_ebp%2Fr17%2F.gitignore;0;2
+FILE_NAME_44=867;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Finclude%2Ftimer.h;0;2
+FILE_NAME_45=2864;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fsrc%2Ftimer.c;0;2
+FILE_NAME_46=60;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Finclude%2Fmem.h;0;2
+FILE_NAME_47=426;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Finclude%2Fstate.h;0;2
+FILE_NAME_48=61;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fsrc%2Fmonolog.c;0;2
+FILE_NAME_49=17892;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Finclude%2Flibtelnet.h;0;2
+FILE_NAME_50=1774;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Finclude%2Fmonolog.h;0;2
+FILE_NAME_51=235;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Finclude%2Fdynar.h;0;2
+FILE_NAME_52=103;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fsrc%2Fmem.c;0;2
+FILE_NAME_53=5343;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fsrc%2Fdynar.c;0;2
+FILE_NAME_54=2919;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Finclude%2Fserver.h;0;2
 
 [VTE]
 last_dir=/home/bjorn/src/woe