Browse Source

Working on telnet implementation. Using an Fiber and select based server.

Beoran 8 years ago
parent
commit
942cc38e0e
16 changed files with 1807 additions and 241 deletions
  1. 1 0
      data/script/serdes.rb
  2. 5 0
      lib/monolog.rb
  3. 69 0
      lib/rfc1143.rb
  4. 481 69
      lib/telnet.rb
  5. 1 1
      lib/telnet/codes.rb
  6. 384 59
      lib/woe/client.rb
  7. 130 0
      lib/woe/cserver.rb
  8. 109 0
      lib/woe/eserver.rb
  9. 197 0
      lib/woe/fserver.rb
  10. 0 0
      lib/woe/log.rb
  11. 123 88
      lib/woe/server.rb
  12. 130 0
      lib/woe/server_doesnt_work.rb
  13. 11 2
      test/test_rfc1143.rb
  14. 134 0
      test/test_telnet.rb
  15. 2 1
      test/woe/test_server.rb
  16. 30 21
      woe.geany

+ 1 - 0
data/script/serdes.rb

@@ -62,6 +62,7 @@ module Serdes
       @serdes_loaded ||= {}
       return @serdes_loaded[id.to_sym]
     end
+        
 
     def serdes_load(id)
       return nil unless id && !id.empty?

+ 5 - 0
lib/monolog.rb

@@ -171,6 +171,11 @@ module Monolog
     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
  

+ 69 - 0
lib/rfc1143.rb

@@ -3,6 +3,8 @@ 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
 
@@ -189,7 +191,74 @@ Upon receipt of WONT, we choose based upon him and himq:
       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
   
 
 
+

+ 481 - 69
lib/telnet.rb

@@ -17,7 +17,7 @@ class Telnet
   # Helper structs
   Telopt = Struct.new(:telopt, :us, :him)
 
-  
+  attr_reader :telopts
   
   def initialize(client)
     @client     = client
@@ -29,28 +29,9 @@ class Telnet
     @compress   = false # compression state
   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)
-    result = ""
-    data.each_byte do | b |
-    iac    = TELNET_IAC.chr  
-      
-      case @buffer
-        when /\A#{iac}#{iac}\Z/
-        
-        # ongoing negotiation
-        when /\A#{iac}\Z/
-          return nil
-        else
-          
-        
-      end
-    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
@@ -60,75 +41,61 @@ class Telnet
     else
       zbuf = buf
     end
-    @client.send_data(zbuf)
+    # 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}")
+    self.send_raw(buf.gsub("#{iac}", "#{iac}#{iac}"))
   end
   
   # Send negotiation bytes
   
-/* send negotiation bytes */
+  # negotiation bytes 
   def send_negotiate(cmd, telopt)
     bytes = ""
     bytes << TELNET_IAC
     bytes << cmd
     bytes << telopt
     send_raw(bytes)
-  end
+  end  
   
-  # Check if we support a particular telopt;
+  # 
+  
+  # Check if we support a particular telsopt using the RFC1143 state
   def us_support(telopt)
-    have = @telopts[telopt] 
+    have = @rfc1143[telopt]
     return false unless have
-    return (have.telopt == telopt) && have.us 
+    return (have.telopt == telopt) && have.us == :yes 
   end
   
-  # Check if the remote supports a telopt
+  # Check if the remote supports a telopt (and it is enabled)
   def him_support(telopt)
-    have = @telopts[telopt] 
+    have = @rfc1143[telopt]
     return false unless have
-    return (have.telopt == telopt) && have.him 
+    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] 
+    @rfc1143[telopt]
   end
-  
-  
+    
   # save RFC1143 option state
-  def rfc1143_set(telopt, us, him)
-    agree = we_support(telopt)
+  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_will(rfc1143)
-    return rfc1143.handle_will
-  end
-  
-  # RFC1143 telnet option negotiation helper
-  def rfc1143_negotiate_wont(rfc1143)
-     return rfc1143.handle_wont
-  end
-  
-  # RFC1143 telnet option negotiation helper
-  def rfc1143_negotiate_do(rfc1143)
-    return rfc1143.handle_do
-  end
-  
-  # RFC1143 telnet option negotiation helper
-  def rfc1143_negotiate_dont(rfc1143)
-    return rfc1143.handle_dont
-  end
-    
   # RFC1143 telnet option negotiation helper
   def rfc1143_negotiate(telopt)
     q = rfc1143_get(telopt)
@@ -136,30 +103,475 @@ class Telnet
     
     case @state
     when :will
-      return rfc1143_negotiate_will(q)    
+      return q.handle_will 
     when :wont
-      return rfc1143_negotiate_wont(q)    
+      return q.handle_wont
     when :do
-      return rfc1143_negotiate_do(q)    
+      return q.handle_do
     when :dont
-      return rfc1143_negotiate_dont(q)    
+      return q.handle_dont
     end  
   end
   
+  # Performs a telnet negotiation
   def do_negotiate(telopt)
     res, arg = rfc1143_negotiate(telopt)
-    return unless res
-    if res == :error
-      log_error(arg)
+    send_event(@state, telopt, res, arg)
+  end
+  
+  
+  # Process a subnegotiation buffer for a naws event
+  def subnegotiate_naws(buffer)
+    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_MSSSP_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
-      send_negotiate(res, arg)
+      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 << byte
+        send_raw(@buffer)
+        @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)
+    if @compress
+      zdat = Zlib.inflate(data)
+    else
+      zdat = data
+    end
+    process_bytes(zdat)
+  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
+
+
+  # send formatted data with \r and \n translation in addition to IAC IAC 
+  def telnet_printf(fmt, *args)
+    crlf  = "\r\n"
+    clnul = "\r\0"
+    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
   
-  # Called when server data should be filtered before being passed to the client
-  def server_to_client(data)
+  # 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
+
 end

+ 1 - 1
lib/telnet/codes.rb

@@ -1,7 +1,7 @@
 # This module contains the contants used for Telnet
 # Based on code by Jon A. Lambert,  under the Zlib license.
  
-module Telnet
+class Telnet
   module Codes
     # Telnet commands
     TELNET_IAC = 255

+ 384 - 59
lib/woe/client.rb

@@ -1,69 +1,295 @@
-require 'eventmachine'
 require 'tempfile'
 require 'fiber'
-require_relative '../monolog'
+require 'timeout'
 
+require_relative '../monolog'
+require_relative '../telnet'
 
 module Woe
 
-class Minifilter
-
-  def initialize(give)
-    @give   = give
-    @fiber  = nil
-  end 
+class Client
+  include Monolog
+  include Telnet::Codes
+  
+  attr_reader :io
+  attr_reader :id
+  # to allow for read timeouts
+  attr_reader :timeout_at
   
-  def wait_for_input(val=nil)
-    return Fiber.yield(val)
+  def initialize(server, id, io)
+    @server = server
+    @id     = id
+    @io     = io
+    @fiber  = Fiber.new { serve }
+    @telnet = Telnet.new(self)    
+    @telnet.set_support(TELNET_TELOPT_NAWS)
+    @telnet.set_support(TELNET_TELOPT_MSSP)
+    @telnet.set_support(TELNET_TELOPT_TTYPE)
+    @telnet.set_support(TELNET_TELOPT_ECHO)
+    @telnet.set_support(TELNET_TELOPT_COMPRESS2)
+    @busy   = true
+    # telnet event queue
+    @telnet_events = []
+    @timeout_at   = nil
   end
   
-  def filter_input(line)
-    if line =~ /2/
-      return (line + line)
+  def alive?
+    @fiber.alive? && @busy
+  end
+  
+  def command(cmd, args)
+    @fiber.resume(cmd, args)
+  end
+  
+  def write(data)
+    @io.write(data)
+  end
+  
+ 
+  def on_start
+     p "Starting client fiber"
+     return nil
+  end
+  
+  def on_write(data)
+      p "On write:"
+      self.write("Client #{socket}:")
+      self.write(args)
+  end
+  
+  # Telnet event class
+  class TelnetEvent
+    attr_accessor :type
+    attr_accessor :data
+    def initialize(type, data)
+      @type = type
+      @data = data
     end
     
-    if line =~ /4/
-      res = wait_for_input
-      return (res * 4)      
+    def to_s
+      "<TelnetEvent #{@type} #{@data}>"    
     end
+  end
+
+  def telnet_event(type, *data)
+    # store in the event queue
+    @telnet_events << TelnetEvent.new(type, data)
+    p "Received tenet event #{@telnet_events}."
+  end
+  
+  def telnet_send_data(buf)
+    # @telnet_events << TelnetEvent.new(:command, buf)
+    p "Sending telnet data."
+    self.write(buf)
+  end
+  
+  def process_telnet_events
     
-    if line =~ /0/
-      return (nil)      
+  end
+  
+  def on_read    
+    data = @io.readpartial(4096)
+    p "After read: #{data}"
+    @io.flush
+    @telnet.telnet_receive(data) 
+    # now, the data and any telnet events are in @telnet_events
+    return data
+  end
+
+
+  # Waits for input from the client.
+  # any
+  # This is always wrapped as a TelnetEvent.
+  # Pure commands have the field type == :command
+  # consisting of a type and a data key in a hash
+  # Pass in nloops to time out the loop a loop
+  # has no definite timing. 
+  def wait_for_input(timeout = nil)    
+    loop do
+      # Timout based on number of loops. 
+      if timeout
+        @timeout_at = Time.now + timeout
+      else
+        @timeout_at = nil
+      end
+      
+      unless @telnet_events.empty?
+        return @telnet_events.shift
+      end
+
+      cmd, arg  = Fiber.yield 
+      data      = nil
+      case cmd 
+      when :start
+        on_start
+      when :timeout
+        @timeout_at = nil
+        return nil
+      when :read
+        data = on_read
+        # all data ends up in he telnet_events queue
+        unless @telnet_events.empty?
+          return @telnet_events.shift
+        end
+      when :write 
+        on_write(arg)
+      else
+        p "Unknown command #{cmd}" 
+      end
+    end
+  end
+  
+  
+  def autohandle_event(tev)
+    case tev.type
+    when :naws
+      @window_h, @window_w = *tev.data
+      log_info("Client #{@id} window size #{@window_w}x#{@window_h}") 
+    else
+      log_info('Telnet event #{tev} ignored')
     end
+  end
+  
+  def wait_for_command(timeout = nil)
+    loop do
+      tevent = wait_for_input(timeout)
+      return nil if tevent.nil?
+      if tevent.type == :data
+        return tevent.data.join('').strip
+      else
+        autohandle_event(tevent)
+      end
+    end
+  end
+          
+  
+  def ask_login
+    @login = nil
+    while  @login.nil? || @login.empty?
+      write("Login:")
+      @login = wait_for_command
+    end
+    @login.chomp!
+    true
+  end
+
+  def ask_password
+    @password = nil
+    while  @password.nil? || @password.empty?
+      write("\r\nPassword:")
+      @password = wait_for_command
+    end
+    @password.chomp!
+    true
+  end
+  
+  def handle_command
+    order = wait_for_command
+    case order
+    when "/quit"
+      write("Byebye!\r\n")
+      @busy = false
+    else
+      @server.broadcast("#@login said #{order}\r\n")
+    end
+  end
+  
+  def setup_naws
+    # Negotiate NAWS (window size) support
+    @telnet.telnet_send_negotiate(TELNET_DO, TELNET_TELOPT_NAWS)
+    tev = wait_for_input(0.5)
+    return nil unless tev
+    ask, cmd, opt = *tev.data
+    return tev unless tev.type == :will
+    tev2 = wait_for_input(0.5)
+    return tev2 unless tev2 && tev2.type == :naws
+    @window_h, @window_w = *tev2.data
+    log_info("Client #{@id} window size #{@window_w}x#{@window_h}") 
+    return nil
+  end
+  
+  def setup_telnet
+    loop do 
+      tev = wait_for_input(0.5)
+      if tev
+        p "setup_telnet", tev
+      else
+        p "no telnet setup received..."
+        break
+      end
+    end
+
+    setup_naws
+    
+    #p "mssp ev #{tev}"
+    # @telnet.telnet_send_negotiate(TELNET_WILL, TELNET_TELOPT_MSSP)        
+    # tev = wait_for_input(0.5)
+    # p "mssp ev #{tev}"
+    
+    # @telnet.telnet_ttype_send
+    
     
-    return line
   end
     
-end
+  def serve()
+    setup_telnet
+    data = nil
+    lok  = ask_login
+    return false unless lok    
+    pok  = ask_password
+    return false unless pok
+    write("\r\nWelcome #{@login} #{@password}!\r\n")
+    while @busy do
+      handle_command
+    end
+  end
 
-class Client < EventMachine::Connection
-  include Monolog
+  
+=begin
 
   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)
+  def initialize(server, id, socket)        
+    @id         = id
+    @server     = server
+    @connected  = true
+    @socket     = socket
+    @telnet     = Telnet.new(self) 
+    @busy       = true
   end
   
-  def post_init()
+  
+  # Get some details about the telnet connection
+  def setup_telnet
+    @telnet.telnet_send_negotiate(TELNET_DO, TELNET_TELOPT_TTYPE)
+    @telnet.telnet_ttype_send
+    type, *args = wait_for_event
+    p type, args
+  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
-    log_info("Client #{@id} connected from #{@ip}:#{@port}")
+    log_info("Client #{@id} connected.")
     self.send_data("Login:")
   end
-      
+  
+  # Send data to the socket
+  def send_data(data)
+    @socket.write(data)
+  end
+  
+  # Run the client's main loop
+  def run
+    post_init
+    while @connected    
+      data = @socket.readpartial(4096)
+      unless data.nil? || data.empty?      
+        receive_data(data) 
+      end
+      p data
+    end
+  end
   
 
     
@@ -84,21 +310,26 @@ class Client < EventMachine::Connection
       self.send_data("Saved.")
     end
     
-    EM.defer(do_save, on_save)    
+    Celluloid.defer(do_save, on_save)
   end
   
+  def wait_for_event
+    return Fiber.yield
+  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 
+  # Any telnet commands are dispatched to the related telnet handlers.
   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
+    loop do
+      type, *args = Fiber.yield
+      if type == :data
+        return args.first
+      else
+        telnet_dispatch(type, *args)
+      end      
+    end
   end
   
   def try
@@ -108,10 +339,14 @@ class Client < EventMachine::Connection
   end
     
   # Fake synchronous handing of input  
-  def handle_input()
+  def handle_input()        
+    setup_telnet
     @login    = wait_for_input
+    
+    self.send_data([TELNET_IAC, TELNET_WILL, TELNET_TELOPT_ECHO].pack('c*'))    
     self.send_data("\nPassword for #{@login}:")
     @password = wait_for_input
+    self.send_data([TELNET_IAC, TELNET_WONT, TELNET_TELOPT_ECHO].pack('c*'))    
     self.send_data("\nOK #{@password}, switching to command mode.\n")
       
     while @connected
@@ -135,11 +370,102 @@ class Client < EventMachine::Connection
       end
     end
   end  
-    
+  
+
   def receive_data(data)
     # Ignore any input if already requested disconnection
     return unless @connected
-    # 
+    @telnet.telnet_receive(data) 
+  end
+  
+  
+  
+  def unbind
+    log_info("Client #{@id} has left from #{@ip}:#{@port}")
+    @server.disconnect(@id)
+  end
+
+  # Called when the telnet module wants to send data.
+  def telnet_send_data(buffer)
+    p "Sending telnet data #{buffer}"
+    self.send_data(buffer)
+  end
+  
+  # Dispatches a telnet event to a function named telnet_(event_name)
+  def telnet_dispatch(type, *args)
+    meth = "telnet_#{type}".to_sym
+    self.send(meth, *args)
+  end
+  
+  
+  # Telnet event handler, called on incoming events.
+  def telnet_event(type, *args)
+    log_info("Telnet event received by client #{id}: #{type}, #{args}")
+    if @fiber
+      # restart the fiber if available
+      @fiber.resume(type, *args)
+    else      
+      # set up a fiber to handle the events
+      # 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 telnet_event_fiber works
+      @fiber.resume()
+      @fiber.resume(type, *args)
+    end    
+  end
+  
+
+
+  
+  # Real handler, called inside a fiber
+  def telnet_event_fiber()
+    raise "not implemented"    
+  end
+    
+  def telnet_environment(fb, vars)
+    p fb,vars
+  end
+  
+  def telnet_environment(fb, vars)  
+    p fb,vars
+  end
+  
+  
+  def telnet_mssp(vars)
+    @mssp_vars = vars
+  end
+  
+  def telnet_ttype_is(term)
+    @term = term
+    self.send_data("\nYou have a #{@term} type terminal.\n")
+    p "term #{@term}"
+  end
+    
+  def telnet_ttype_send(term)
+    p "term #{term} sent"
+  end
+  
+  
+  def telnet_compress(compress)  
+    p "compress #{compress} set"
+  end  
+    
+    
+  def telnet_subnegotiate(sb_telopt, buffer)
+    p "received subnegotiate #{sb_telopt} #{buffer}"
+  end  
+  
+  
+  
+  def do_main
+  end
+  
+  
+  def telnet_data(data)
+=begin  
+    # send data over telnet protocol. Should arrive below in telnet_data
     if @fiber
       @fiber.resume(data)
     else      
@@ -152,16 +478,15 @@ class Client < EventMachine::Connection
       @fiber.resume()
       @fiber.resume(data)
     end    
-  end
-  
-  
+  end  
   
-  def unbind
-    log_info("Client #{@id} has left from #{@ip}:#{@port}")
-    @server.disconnect(@id)
-  end
-end
+  def telnet_iac(byte)  
+    p "received  iac #{byte}"
+  end  
+=end
+
+  end # class Client
 
-end
+end # module Woe
 
 

+ 130 - 0
lib/woe/cserver.rb

@@ -0,0 +1,130 @@
+require 'tempfile'
+require 'fiber'
+require_relative '../monolog'
+
+require 'celluloid/io'
+require 'celluloid/autostart'
+
+
+Celluloid.task_class = Celluloid::Task::Threaded
+
+module Woe
+  class Server     
+    include Monolog
+    
+    include Celluloid::IO
+    finalizer :shutdown
+ 
+    def initialize(host = 'localhost', port =7000, logname="woe.log")
+      Monolog.setup_all(logname)
+      # Celluloid.logger = self
+      @port      = port
+      # Keep an overall record of the client IDs allocated
+      # and the lines of chat
+      @client_id = 0
+      @clients   = {}
+      @tick_id   = 0
+      @host      = host
+      p "Server listening on #@host port #@port"
+      @server    = Celluloid::IO::TCPServer.new(@host, @port)
+      async.run
+    end
+    
+    def get_free_client_id
+      cli = 0
+      @clients.each do |client|
+        return cli if client.nil?
+        cli += 1
+      end
+      return cli
+    end
+    
+    def run
+      @busy = true
+      p "Server main loop starts."
+      while @busy
+        begin
+          p "Accepting"
+          socket = @server.accept
+          p socket
+          async.handle_connection(socket)  
+        rescue 
+          p "exception #{$!}"
+          @busy = false
+        end
+      end
+    end    
+    
+    def handle_connection(socket)
+      p "Connecting socket."
+       _, port, host = socket.peeraddr
+      p "*** Received connection from #{host}:#{port}"
+
+      client_id            = get_free_client_id
+      client               = Client.new(self, client_id, socket)   
+      @clients[client_id]  = client
+      begin
+        client.run        
+      rescue EOFError
+        p "*** #{host}:#{port} disconnected"
+      ensure
+        disconnect(client.id)
+        socket.close
+      end
+    end  
+    
+    
+    def disconnect(id)
+      log_info("Server disconnecting client #{@id}")
+      @clients.delete(id)
+    end
+    
+    def clients_stopped?
+    end
+    
+    def reload
+      log_info("Server reload")
+      broadcast("Server reload NOW!\n")
+      begin 
+        load 'lib/telnet.rb'
+        load 'lib/woe/client.rb'
+        load 'lib/woe/server.rb'
+        broadcast("Server reloaded OK.\n")
+      rescue Exception => ex
+        bt = ex.backtrace.join("\n")
+        log_error("Server reload failed: #{ex}: #{bt}")
+        broadcast("Server reload exception #{ex}: #{bt}!\n")
+      end
+    end
+    
+    def stop
+      log_info("Server stop")
+      shutdown
+      log_info("Server stop OK.")      
+    end
+    
+   
+    def broadcast(msg)
+      @clients.each do |id, client|
+        client.send_data(msg)
+      end
+    end
+    
+   
+    def shutdown
+      log_info("Shuting down server.")
+      @busy = false
+      @server.close if @server
+    end    
+  end
+end
+
+
+
+
+
+
+
+
+
+

+ 109 - 0
lib/woe/eserver.rb

@@ -0,0 +1,109 @@
+require 'eventmachine'
+require 'tempfile'
+require 'fiber'
+require_relative '../monolog'
+
+
+
+module Woe
+  class Server 
+    include Monolog
+  
+    def initialize(port =7000, logname="woe.log")
+      Monolog.setup_all(logname)
+      @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 get_free_client_id
+      cli = 0
+      @clients.each do |client|
+        return cli if client.nil?
+        cli += 1
+      end
+      return cli
+    end
+    
+    def start() 
+      log_info("Server listening on port #@port")
+      @signature = EventMachine.start_server("0.0.0.0", @port, Client) do |client|
+        client_id            = get_free_client_id
+        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
+      log_info("Server main loop starts.")
+      EventMachine.run do       
+        self.start
+      end
+    end
+    
+    
+    def disconnect(id)
+      log_info("Server disconnecting client #{@id}")
+      @clients.delete(id)
+    end
+    
+    def clients_stopped?
+    end
+    
+    def reload
+      log_info("Server reload")
+      broadcast("Server reload NOW!\n")
+      begin 
+        load 'lib/telnet.rb'
+        load 'lib/woe/client.rb'
+        load 'lib/woe/server.rb'
+        broadcast("Server reloaded OK.\n")
+      rescue Exception => ex
+        bt = ex.backtrace.join("\n")
+        log_error("Server reload failed: #{ex}: #{bt}")
+        broadcast("Server reload exception #{ex}: #{bt}!\n")
+      end
+    end
+    
+    def stop
+      log_info("Server stop")
+      EventMachine.stop_server(@signature)
+      EventMachine.add_timer(1) do 
+        EventMachine.stop
+        log_info("Server stop OK.")
+      end
+    end
+    
+   
+    def broadcast(msg)
+      @clients.each do |id, client|
+        client.send_data(msg)
+      end
+    end
+    
+
+    def self.run(port=7000, logname="woe.log")    
+      server = Woe::Server.new(port, logname)
+      server.run
+    end
+      
+  end
+end
+
+
+
+
+
+
+
+

+ 197 - 0
lib/woe/fserver.rb

@@ -0,0 +1,197 @@
+require 'socket'
+require 'fiber'
+include Socket::Constants
+
+
+class FServer
+  def initialize
+    @timers     = {}
+    @reading    = []
+    @writing    = []
+    @clients    = {}
+    @client_id  = 0
+  end
+  
+  class Client
+    attr_reader :io
+    attr_reader :id
+    
+    def initialize(server, id, io)
+      @server = server
+      @id     = id
+      @io     = io
+      @fiber  = Fiber.new { serve } 
+      @busy   = true
+    end
+    
+    def alive?
+      @fiber.alive? && @busy
+    end
+    
+    def command(cmd, args)
+      @fiber.resume(cmd, args)
+    end
+    
+    def write(data)
+      @io.write(data)
+    end
+    
+   
+    def on_start
+       p "Starting client fiber"
+       return nil
+    end
+    
+    def on_write(data)
+        p "On write:"
+        self.write("Client #{socket}:")
+        self.write(args)
+    end
+    
+    def on_read    
+      data = @io.readpartial(4096)
+      p "After read: #{data}"
+      @io.flush
+      return data
+    end
+
+  
+    def wait_for_input
+      loop do
+        cmd, arg  = Fiber.yield 
+        data      = nil
+        case cmd 
+        when :start
+          on_start
+        when :read
+          return on_read
+        when :write 
+          on_write(arg)
+        else
+          p "Unknown command #{cmd}" 
+        end
+      end
+    end
+            
+
+    
+    def ask_login
+      @login = nil
+      while  @login.nil? || @login.empty?
+        write("Login:")
+        @login = wait_for_input.chomp
+      end
+      true
+    end
+
+    def ask_password
+      @password = nil
+      while  @password.nil? || @password.empty?
+        write("\r\nPassword:")
+        @password = wait_for_input.chomp
+      end
+      true
+    end
+    
+    def handle_command
+      order = wait_for_input.chomp
+      case order
+      when "/quit"
+        write("Byebye!\r\n")
+        @busy = false
+      else
+        @server.broadcast("#@login said #{order}\r\n")
+      end
+    end
+    
+    
+    def serve()
+      data = nil
+      lok  = ask_login
+      return false unless lok    
+      pok  = ask_password
+      return false unless pok
+      write("\r\nWelcome #{@login} #{@password}!\r\n")
+      while @busy do
+        handle_command
+      end
+    end
+  end
+  
+  def start
+    @server = TCPServer.new('localhost', 7000)
+    @reading << @server
+    serve
+  end
+
+  def add_client
+    socket               = @server.accept_nonblock
+    @reading          << socket
+    @client_id          += 1
+    client               = Client.new(self, @client_id, socket)
+    @clients[socket]     = client
+    client.command(:start, nil)
+    puts "Client #{socket} connected"
+    return client
+  end
+
+  def broadcast(message)
+    @clients.each_pair do | id, client |
+      client.write(message + "\r\n")
+    end
+  end
+  
+  def add_timer(id, delta = 1)
+    now   = Time.now
+    stop  = now + delta
+    @timers[id.to_sym]  = stop
+  end
+  
+  def serve
+    add_timer(:test, 15)
+    loop do
+      # Kick out clients that disconnected.
+      disconnected = @clients.reject { |c, cl| cl.alive? }
+      disconnected.each do |c, cl|
+        rsock = c
+        @clients.delete(rsock)
+        @reading.delete(rsock)
+        rsock.close
+      end
+      
+      # Nodify timers that expired
+      now     = Time.now
+      expired = @timers.select { |k, t| t < now } 
+      expired.each do |k, t|
+        broadcast("Timer #{k} expired: #{t}, #{now}.\n\r")
+        @timers.delete(k)
+      end
+            
+    
+      readable, writable = IO.select(@reading, @writing, nil, 0.1)
+      if readable
+        readable.each do | rsock |
+          if rsock == @server
+            add_client
+          # Kick out clients with broken connections.
+          elsif rsock.eof?
+            @clients.delete(rsock)
+            @reading.delete(rsock)
+            rsock.close
+          else
+            # Tell the client to get their read on.              
+            client  = @clients[rsock]
+            text    = client.command(:read, nil)
+          end
+        end
+      end
+    end
+  end
+  
+end
+
+server = FServer.new
+server.start
+
+
+

+ 0 - 0
lib/woe/log.rb


+ 123 - 88
lib/woe/server.rb

@@ -1,106 +1,141 @@
-require 'eventmachine'
-require 'tempfile'
+require 'socket'
 require 'fiber'
+require 'timeout'
+
 require_relative '../monolog'
 
 
+
 module Woe
-  class Server 
-    include Monolog
+class Server
+  include Socket::Constants
+  include Monolog
   
-    def initialize(port =7000, logname="woe.log")
-      Monolog.setup_all(logname)
-      @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 get_free_client_id
-      cli = 0
-      @clients.each do |client|
-        return cli if client.nil?
-        cli += 1
-      end
-      return cli
-    end
-    
-    def start() 
-      log_info("Server listening on port #@port")
-      @signature = EventMachine.start_server("0.0.0.0", @port, Client) do |client|
-        client_id            = get_free_client_id
-        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  
+  def initialize(port = 7000, host='0.0.0.0')
+    @port       = port
+    @host       = host
+    @timers     = {}
+    @reading    = []
+    @writing    = []
+    @clients    = {}
+    @client_id  = 0
+  end
+  
+  # Look for a free numeric client ID in the client hash.
+  def get_free_client_id
+    cli = 0
+    @clients.each do |client|
+      return cli if client.nil?
+      cli += 1
     end
-    
-    
-    def run
-      log_info("Server main loop starts.")
-      EventMachine.run do       
-        self.start
-      end
+    return cli
+  end
+  
+  def run
+    log_info("Starting server on #{@host} #{@port}.")
+    @server = TCPServer.new(@host, @port)
+    @reading << @server
+    serve
+  end
+
+  def add_client
+    socket               = @server.accept_nonblock
+    @reading          << socket
+    client_id            = get_free_client_id 
+    client               = Client.new(self, client_id, socket)
+    @clients[client_id]  = client
+    client.command(:start, nil)
+    puts "Client #{client.id} connected on #{socket}."
+    return client
+  end
+
+  def broadcast(message)
+    @clients.each_pair do | id, client |
+      client.write(message + "\r\n")
     end
-    
-    
-    def disconnect(id)
-      log_info("Server disconnecting client #{@id}")
+  end
+  
+  def add_timer(id, delta = 1)
+    now   = Time.now
+    stop  = now + delta
+    @timers[id.to_sym]  = stop
+  end
+  
+  
+  # Nodify timers that expired
+  def handle_disconnected
+   # Kick out clients that disconnected.
+    disconnected = @clients.reject { |id, cl| cl.alive? }
+    disconnected.each do |id, cl|
+      rsock = cl.io
       @clients.delete(id)
+      @reading.delete(rsock)
+      rsock.close
+    end     
+  end
+  
+  # Handle timers
+  def handle_timers
+    now     = Time.now
+    expired = @timers.select { |k, t| t < now } 
+    expired.each do |k, t|
+      broadcast("Timer #{k} expired: #{t}, #{now}.\n\r")
+      @timers.delete(k)
     end
-    
-    def clients_stopped?
-    end
-    
-    def reload
-      log_info("Server reload")
-      broadcast("Server reload NOW!\n")
-      begin 
-        load 'lib/woe/server.rb'
-        broadcast("Server reloaded OK.\n")
-      rescue Exception => ex
-        bt = ex.backtrace.join("\n")
-        log_error("Server reload failed: #{ex}: #{bt}")
-        broadcast("Server reload exception #{ex}: #{bt}!\n")
-      end
+  end
+  
+  def client_for_socket(sock)
+    @clients.each do |k, c| 
+      return c if c.io == sock 
     end
-    
-    def stop
-      log_info("Server stop")
-      EventMachine.stop_server(@signature)
-      EventMachine.add_timer(1) do 
-        EventMachine.stop
-        log_info("Server stop OK.")
+    return nil
+  end
+  
+   # Nodify clients that havea read timeout set
+  def handle_timeouts
+    now = Time.now
+    @clients.each  do |id, cl| 
+      if cl.timeout_at && cl.timeout_at > now
+        cl.command(:timeout, nil)
       end
     end
-    
-   
-    def broadcast(msg)
-      @clients.each do |id, client|
-        client.send_data(msg)
+  end
+ 
+  def serve
+    @busy = true
+    while @busy
+      handle_disconnected
+      handle_timers
+      handle_timeouts
+            
+      readable, writable = IO.select(@reading, @writing, nil, 0.1)
+      if readable
+        readable.each do | rsock |
+          if rsock == @server
+            add_client
+          # Kick out clients with broken connections.
+          elsif rsock.eof?
+            @clients.delete(client_for_socket(rsock))
+            @reading.delete(rsock)
+            rsock.close
+          else
+            # Tell the client to get their read on.              
+            client  = client_for_socket(rsock)
+            text    = client.command(:read, nil)
+          end
+        end
       end
     end
-    
-
-    def self.run(port=7000, logname="woe.log")    
-      server = Woe::Server.new(port, logname)
-      server.run
-    end
-      
+  end  
+  
+  def self.run(port = 7000, host = '0.0.0.0', logname = 'woe.log')
+    Monolog.setup_all(logname)
+    server = self.new(port, host)
+    server.run
+    Monolog.close
   end
-end
-
-
-
-
-
-
+  
+end # class Server
+end # module Woe
 
 

+ 130 - 0
lib/woe/server_doesnt_work.rb

@@ -0,0 +1,130 @@
+require 'tempfile'
+require 'fiber'
+require_relative '../monolog'
+
+require 'celluloid/io'
+require 'celluloid/autostart'
+
+
+Celluloid.task_class = Celluloid::Task::Threaded
+
+module Woe
+  class Server     
+    include Monolog
+    
+    include Celluloid::IO
+    finalizer :shutdown
+ 
+    def initialize(host = 'localhost', port =7000, logname="woe.log")
+      Monolog.setup_all(logname)
+      # Celluloid.logger = self
+      @port      = port
+      # Keep an overall record of the client IDs allocated
+      # and the lines of chat
+      @client_id = 0
+      @clients   = {}
+      @tick_id   = 0
+      @host      = host
+      p "Server listening on #@host port #@port"
+      @server    = Celluloid::IO::TCPServer.new(@host, @port)
+      async.run
+    end
+    
+    def get_free_client_id
+      cli = 0
+      @clients.each do |client|
+        return cli if client.nil?
+        cli += 1
+      end
+      return cli
+    end
+    
+    def run
+      @busy = true
+      p "Server main loop starts."
+      while @busy
+        begin
+          p "Accepting"
+          socket = @server.accept
+          p socket
+          async.handle_connection(socket)  
+        rescue 
+          p "exception #{$!}"
+          @busy = false
+        end
+      end
+    end    
+    
+    def handle_connection(socket)
+      p "Connecting socket."
+       _, port, host = socket.peeraddr
+      p "*** Received connection from #{host}:#{port}"
+
+      client_id            = get_free_client_id
+      client               = Client.new(self, client_id, socket)   
+      @clients[client_id]  = client
+      begin
+        client.run        
+      rescue EOFError
+        p "*** #{host}:#{port} disconnected"
+      ensure
+        disconnect(client.id)
+        socket.close
+      end
+    end  
+    
+    
+    def disconnect(id)
+      log_info("Server disconnecting client #{@id}")
+      @clients.delete(id)
+    end
+    
+    def clients_stopped?
+    end
+    
+    def reload
+      log_info("Server reload")
+      broadcast("Server reload NOW!\n")
+      begin 
+        load 'lib/telnet.rb'
+        load 'lib/woe/client.rb'
+        load 'lib/woe/server.rb'
+        broadcast("Server reloaded OK.\n")
+      rescue Exception => ex
+        bt = ex.backtrace.join("\n")
+        log_error("Server reload failed: #{ex}: #{bt}")
+        broadcast("Server reload exception #{ex}: #{bt}!\n")
+      end
+    end
+    
+    def stop
+      log_info("Server stop")
+      shutdown
+      log_info("Server stop OK.")      
+    end
+    
+   
+    def broadcast(msg)
+      @clients.each do |id, client|
+        client.send_data(msg)
+      end
+    end
+    
+   
+    def shutdown
+      log_info("Shuting down server.")
+      @busy = false
+      @server.close if @server
+    end    
+  end
+end
+
+
+
+
+
+
+
+
+
+

+ 11 - 2
test/test_rfc1143.rb

@@ -3,6 +3,8 @@ include Atto::Test
 
 require_relative '../lib/rfc1143' 
 
+include Telnet::Codes
+
 assert { RFC1143 }
 
 sm = RFC1143.new(:echo, :no, :no, true)
@@ -16,14 +18,21 @@ assert { sm.agree   == true }
 assert do 
   sm = RFC1143.new(:echo, :no, :no, true)
   res, arg = sm.handle_will
-  res == :send_do
+  res == TELNET_DO
   arg == :echo
 end
 
 assert do 
   sm = RFC1143.new(:echo, :no, :no, false)
   res, arg = sm.handle_will
-  res == :send_dont
+  res == TELNET_DONT
+  arg == :echo
+end
+
+assert do 
+  sm = RFC1143.new(:echo, :no, :no, false)
+  res, arg = sm.send_will
+  res == TELNET_WILL
   arg == :echo
 end
 

+ 134 - 0
test/test_telnet.rb

@@ -0,0 +1,134 @@
+require 'atto'
+include Atto::Test
+
+require_relative '../lib/telnet'
+require_relative '../lib/rfc1143' 
+
+include Telnet::Codes
+
+assert { Telnet } 
+
+class TestClient
+  attr_reader :buffer
+  attr_reader :iac
+  attr_reader :out
+
+  def initialize
+    @buffer = ''
+    @iac    = nil
+    @out    = ''
+  end
+
+
+
+  # Telnet event handlers
+  def telnet_event(type, *args)
+    puts("Telnet event received by client: #{type}, #{args}")
+    meth = "telnet_#{type}".to_sym
+    self.send(meth, *args)
+  end
+  
+  def telnet_send_data(zbuf)    
+    @out << zbuf
+  end
+  
+  def telnet_environment(fb, vars)
+    p fb,vars
+  end
+  
+  def telnet_environment(fb, vars)  
+    p fb,vars
+  end
+  
+  
+  def telnet_mssp(vars)
+    @mssp_vars = vars
+  end
+  
+  def telnet_ttype_is(term)
+    @term = term
+    p "term #{@term}"
+  end
+    
+  def telnet_ttype_send(term)
+    p "term #{term} sent"
+  end
+  
+  
+  def telnet_compress(compress)  
+    p "compress #{compress} set"
+  end  
+    
+    
+  def telnet_subnegotiate(sb_telopt, buffer)
+    p "received subnegotiate #{sb_telopt} #{buffer}"
+  end  
+  
+  def telnet_data(data)
+    @buffer << data
+    p "received data #{data}"
+  end  
+  
+  def telnet_iac(byte)  
+    p "received iac #{byte}"
+  end 
+
+  def telnet_will(opt)  
+    p "received will #{opt}"
+  end 
+  
+  def telnet_do(opt)  
+    p "received do #{opt}"
+  end 
+  
+  def telnet_wont(opt)  
+    p "received wont #{opt}"
+  end 
+  
+  def telnet_dont(opt)  
+    p "received dont #{opt}"
+  end 
+
+end
+
+
+
+
+assert do
+  cl = TestClient.new()
+  tn = Telnet.new(cl)
+  tn
+end
+
+assert do
+  cl = TestClient.new()
+  tn = Telnet.new(cl)
+  tn.telnet_receive("Hello")
+  tn.telnet_receive(" World")
+  cl.buffer == "Hello World"
+end
+
+
+assert do
+  cl = TestClient.new()
+  tn = Telnet.new(cl)
+  tn.telnet_receive([TELNET_IAC, TELNET_TELOPT_ECHO].pack('c*'))  
+end
+
+
+assert do
+  cl = TestClient.new()
+  tn = Telnet.new(cl)
+  tn.telnet_send_negotiate(TELNET_DO, TELNET_TELOPT_TTYPE)
+  p cl.out
+end
+
+assert do
+  cl = TestClient.new()
+  tn = Telnet.new(cl)
+  tn.telnet_receive([TELNET_IAC, TELNET_WILL, TELNET_TELOPT_NAWS].pack('c*'))
+  p cl.out
+end
+
+
+

+ 2 - 1
test/woe/test_server.rb

@@ -8,7 +8,8 @@ assert { Woe::Server }
 
 # Fork off the server so the Net::Telnet tests can procede
 pid = Process.fork do 
-  Woe::Server.run
+  Woe::Server.new
+  sleep
   # exit here to ge
   puts __FILE__ + ' Server Done'
   exit 0

+ 30 - 21
woe.geany

@@ -23,13 +23,13 @@ long_line_behaviour=1
 long_line_column=80
 
 [files]
-current_page=33
+current_page=39
 FILE_NAME_0=11178;C;0;EUTF-8;0;1;0;%2Fusr%2Flocal%2Finclude%2Fmruby.h;0;2
 FILE_NAME_1=8011;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Flibtelnet%2Futil%2Ftelnet-chatd.c;0;2
 FILE_NAME_2=697;None;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2FTupfile;0;2
 FILE_NAME_3=235;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fdata%2Fscript%2Fmain.rb;0;2
 FILE_NAME_4=602;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fdata%2Fscript%2Fsecurity.rb;0;2
-FILE_NAME_5=1315;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fdata%2Fscript%2Fserdes.rb;0;2
+FILE_NAME_5=1301;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fdata%2Fscript%2Fserdes.rb;0;2
 FILE_NAME_6=521;None;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fdata%2Fvar%2Ftry.sitef;0;2
 FILE_NAME_7=143;Markdown;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fureqma%2FREADME.md;0;2
 FILE_NAME_8=341;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fureqma%2Fureqma;0;2
@@ -50,34 +50,43 @@ FILE_NAME_22=6413;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Farch%2Fdl%2Fmud%2Freleas
 FILE_NAME_23=2622;YAML;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Farch%2Fdl%2Fmud%2Frelease%2Ftmud-3.0.0%2Fconfig.yaml;0;2
 FILE_NAME_24=0;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fdata%2Fscript%2Fmode%2Fnormal.rb;0;2
 FILE_NAME_25=814;Sh;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fwork%2Febp2git%2Febp_make_all_gitignores;0;2
-FILE_NAME_26=3707;Sh;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fwork%2Febp2git%2Febp_make_gitignore;0;2
+FILE_NAME_26=5662;Sh;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fwork%2Febp2git%2Febp_make_gitignore;0;2
 FILE_NAME_27=1452;None;0;EUTF-8;0;1;0;%2Fusers%2Fnmbs_ebp%2Fr17%2F.gitignore;0;2
 FILE_NAME_28=0;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Flib%2Fmonolog.rb;0;2
 FILE_NAME_29=0;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Flib%2Fsitef.rb;0;2
-FILE_NAME_30=1;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Flib%2Ftelnet.rb;0;2
-FILE_NAME_31=3786;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Flib%2Fserdes.rb;0;2
-FILE_NAME_32=2505;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fwork%2Fatw%2Fscrapeme%2Fatw_logs_scraper.rb;0;2
-FILE_NAME_33=1362;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Flib%2Fwoe%2Fsettings.rb;0;2
-FILE_NAME_34=30175567;None;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fwork%2Fatw%2Fscrapeme%2Fatw2-prod_20150622093131.csv;0;2
-FILE_NAME_35=252;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Flib%2Fwoe%2Faccount.rb;0;2
-FILE_NAME_36=0;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Flib%2Fwoe%2Fansi.rb;0;2
-FILE_NAME_37=3492;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Flib%2Fwoe%2Fclient.rb;0;2
-FILE_NAME_38=0;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Flib%2Fwoe%2Fgserver.rb;0;2
-FILE_NAME_39=0;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Flib%2Fwoe%2Flog.rb;0;2
-FILE_NAME_40=902;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Flib%2Fwoe%2Fserver.rb;0;2
-FILE_NAME_41=0;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Ftest%2Ftest_monolog.rb;0;2
-FILE_NAME_42=0;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Ftest%2Ftest_sitef.rb;0;2
-FILE_NAME_43=352;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Ftest%2Fwoe%2Ftest_server.rb;0;2
-FILE_NAME_44=11;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fatto%2Flib%2Fatto%2Frun.rb;0;2
-FILE_NAME_45=1597;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fatto%2Flib%2Fatto%2Ftest.rb;0;2
-FILE_NAME_46=0;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fbin%2Fwoe;0;2
+FILE_NAME_30=186;None;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2FREADME;0;2
+FILE_NAME_31=126;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Flib%2Ftelnet%2Fcodes.rb;0;2
+FILE_NAME_32=2335;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Flib%2Fserdes.rb;0;2
+FILE_NAME_33=1;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Ftest%2Ftest_serdes.rb;0;2
+FILE_NAME_34=1389;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Flibtelnet%2Flibtelnet.c;0;2
+FILE_NAME_35=11028;C;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Flibtelnet%2Flibtelnet.h;0;2
+FILE_NAME_36=2505;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fwork%2Fatw%2Fscrapeme%2Fatw_logs_scraper.rb;0;2
+FILE_NAME_37=252;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Flib%2Fwoe%2Faccount.rb;0;2
+FILE_NAME_38=0;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Flib%2Fwoe%2Fansi.rb;0;2
+FILE_NAME_39=2196;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Flib%2Fwoe%2Fclient.rb;0;2
+FILE_NAME_40=0;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Flib%2Fwoe%2Fgserver.rb;0;2
+FILE_NAME_41=1696;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Flib%2Fwoe%2Fserver.rb;0;2
+FILE_NAME_42=2724;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Flib%2Fwoe%2Fcserver.rb;0;2
+FILE_NAME_43=923;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Flib%2Fwoe%2Ffserver.rb;0;2
+FILE_NAME_44=0;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Ftest%2Ftest_monolog.rb;0;2
+FILE_NAME_45=722;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Ftest%2Fwoe%2Ftest_settings.rb;0;2
+FILE_NAME_46=366;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Flib%2Fwoe%2Fsettings.rb;0;2
+FILE_NAME_47=558;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Ftest%2Ftest_sitef.rb;0;2
+FILE_NAME_48=0;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Ftest%2Ftest_rfc1143.rb;0;2
+FILE_NAME_49=319;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Ftest%2Ftest_telnet.rb;0;2
+FILE_NAME_50=11112;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Flib%2Ftelnet.rb;0;2
+FILE_NAME_51=6705;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Flib%2Frfc1143.rb;0;2
+FILE_NAME_52=90;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Ftest%2Fwoe%2Ftest_server.rb;0;2
+FILE_NAME_53=11;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fatto%2Flib%2Fatto%2Frun.rb;0;2
+FILE_NAME_54=1597;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fatto%2Flib%2Fatto%2Ftest.rb;0;2
+FILE_NAME_55=147;Ruby;0;EUTF-8;0;1;0;%2Fhome%2Fbjorn%2Fsrc%2Fwoe%2Fbin%2Fwoe;0;2
 
 [VTE]
 last_dir=/home/bjorn/src/woe
 
 [build-menu]
 EX_00_LB=_Execute
-EX_00_CM=bin/woe-server
+EX_00_CM=bin/woe
 EX_00_WD=%p
 NF_00_LB=_Tup
 NF_00_CM=tup