Przeglądaj źródła

Implemented NAWS, COMPRESS2 and MSSP.

Beoran 8 lat temu
rodzic
commit
0e931c1614
3 zmienionych plików z 143 dodań i 25 usunięć
  1. 33 9
      lib/telnet.rb
  2. 51 13
      lib/woe/client.rb
  3. 59 3
      lib/woe/server.rb

+ 33 - 9
lib/telnet.rb

@@ -27,6 +27,18 @@ class Telnet
     @state      = :data # state of telnet protocol parser.
     @sb_telopt  = nil;  # current subnegotiation
     @compress   = false # compression state
+    @zdeflate   = Zlib::Deflate.new() # Deflate stream for compression2 support.
+    @zinflate   = Zlib::Inflate.new() # Inflate stream for compression2 support.
+  end
+  
+  # Closes the telnet connection, send last compressed data if needed.
+  def close
+    if @compress 
+      zbuf = @zdeflate.flush(Zlib::FINISH)
+      @client.telnet_send_data(zbuf)
+    end
+    @zdeflate.close
+    @zinflate.close    
   end
   
   # Send an event to the client to notify it of a state change or of data
@@ -37,7 +49,9 @@ class Telnet
   # Sends unescaped data to client, possibly compressing it if needed
   def send_raw(buf)
     if @compress
-      zbuf = Zlib.deflate(buf)
+      @zdeflate << buf
+      zbuf = @zdeflate.flush(Zlib::SYNC_FLUSH)
+      p "Deflating #{buf} -> #{zbuf}"
     else
       zbuf = buf
     end
@@ -242,7 +256,7 @@ def subnegotiate_mssp(buffer)
         var = ""
         val = ""
       end      
-    when TELNET_MSSSP_VAL
+    when TELNET_MSSP_VAL
       mstate = :val
     else
       if mstate == :var
@@ -306,7 +320,7 @@ end
 # must be aborted and reprocessed due to COMPRESS2 being activated
 
 def do_subnegotiate(buffer)
-  case @sb_telopt
+  case @sb_teloptTELNET_MSSSP_VAR
   when TELNET_TELOPT_COMPRESS2
     # received COMPRESS2 begin marker, setup our zlib box and
     # start handling the compressed stream if it's not already.
@@ -460,12 +474,10 @@ 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)
+    # the COMPRESS2 protocol seems to be half-duplex in that only 
+    # the server's data stream is compressed (unless maybe if the client
+    # is asked to also compress with a DO command ?)
+    process_bytes(data)
   end
   
   # Send a bytes array (raw) to the client
@@ -573,5 +585,17 @@ end
     telnet_send_bytes(TELNET_IAC, TELNET_SB, TELNET_TELOPT_TTYPE, TELNET_TTYPE_IS)
     telnet_send(ttype)
   end
+  
+  # send MSSP data
+  def telnet_send_mssp(mssp)
+    buf = ""
+    mssp.each do | key, val| 
+      buf << TELNET_MSSP_VAR.chr
+      buf << key
+      buf << TELNET_MSSP_VAL.chr
+      buf << val      
+    end
+    telnet_subnegotiation(TELNET_TELOPT_MSSP, buf)
+  end
 
 end

+ 51 - 13
lib/woe/client.rb

@@ -30,7 +30,13 @@ class Client
     @busy   = true
     # telnet event queue
     @telnet_events = []
-    @timeout_at   = nil
+    @timeout_at   = nil    
+  end
+  
+  # Closes up the client
+  def close
+    @telnet.close
+    @io.close
   end
   
   def alive?
@@ -41,10 +47,15 @@ class Client
     @fiber.resume(cmd, args)
   end
   
-  def write(data)
+  def write_raw(data)
     @io.write(data)
   end
   
+  
+  def write(data)
+    @telnet.send_escaped(data)
+  end
+  
  
   def on_start
      p "Starting client fiber"
@@ -80,7 +91,7 @@ class Client
   def telnet_send_data(buf)
     # @telnet_events << TelnetEvent.new(:command, buf)
     p "Sending telnet data."
-    self.write(buf)
+    self.write_raw(buf)
   end
   
   def process_telnet_events
@@ -88,8 +99,7 @@ class Client
   end
   
   def on_read    
-    data = @io.readpartial(4096)
-    p "After read: #{data}"
+    data = @io.readpartial(4096)    
     @io.flush
     @telnet.telnet_receive(data) 
     # now, the data and any telnet events are in @telnet_events
@@ -146,7 +156,7 @@ class Client
       @window_h, @window_w = *tev.data
       log_info("Client #{@id} window size #{@window_w}x#{@window_h}") 
     else
-      log_info('Telnet event #{tev} ignored')
+      log_info("Telnet event #{tev} ignored")
     end
   end
   
@@ -194,13 +204,28 @@ class Client
     end
   end
   
-  def setup_naws
-    # Negotiate NAWS (window size) support
-    @telnet.telnet_send_negotiate(TELNET_DO, TELNET_TELOPT_NAWS)
+  # generic negotiation
+  def setup_negotiate(command, option, yes_event, no_event)
+    @telnet.telnet_send_negotiate(command, option)
     tev = wait_for_input(0.5)
-    return nil unless tev
-    ask, cmd, opt = *tev.data
-    return tev unless tev.type == :will
+    return false, nil unless tev
+    return false, nil if tev.type == no_event
+    return false, tev unless tev.type == yes_event && tev.data[0] == option
+    return true, nil
+  end
+  
+  # Negotiate COMPRESS2 support
+  def setup_compress2
+    ok, tev = setup_negotiate(TELNET_WILL, TELNET_TELOPT_COMPRESS2, :do, :dont)
+    return tev unless ok    
+    @telnet.telnet_begin_compress2
+    log_info("Client #{@id} started COMPRESS2 compression")
+  end
+  
+  # Negotiate NAWS (window size) support
+  def setup_naws  
+    ok, tev = setup_negotiate(TELNET_DO, TELNET_TELOPT_NAWS, :will, :wont)
+    return tev unless ok
     tev2 = wait_for_input(0.5)
     return tev2 unless tev2 && tev2.type == :naws
     @window_h, @window_w = *tev2.data
@@ -208,6 +233,18 @@ class Client
     return nil
   end
   
+  
+  # Negotiate MSSP (mud server status protocol) support
+  def setup_mssp
+    ok, tev = setup_negotiate(TELNET_WILL, TELNET_TELOPT_MSSP, :do, :dont)    
+    return tev unless ok
+    mssp = @server.mssp
+    @telnet.telnet_send_mssp(mssp)
+    return nil
+  end
+  
+  
+  
   def setup_telnet
     loop do 
       tev = wait_for_input(0.5)
@@ -218,7 +255,8 @@ class Client
         break
       end
     end
-
+    setup_mssp
+    setup_compress2
     setup_naws
     
     #p "mssp ev #{tev}"

+ 59 - 3
lib/woe/server.rb

@@ -1,6 +1,7 @@
 require 'socket'
 require 'fiber'
 require 'timeout'
+require 'time'
 
 require_relative '../monolog'
 
@@ -19,8 +20,62 @@ class Server
     @writing    = []
     @clients    = {}
     @client_id  = 0
+    # Used for MSSP 
+    @mssp       = {
+      "NAME"        => "Workers Of Eruta",
+      "UPTIME"      => Time.now.to_i.to_s,
+      "PLAYERS"     => "0",
+      "CRAWL DELAY" => "0",
+      "CODEBASE"    => "WOE",
+      "CONTACT"     => "beoran@gmail.com",
+      "CREATED"     => "2015",
+       "ICON"       => "None",
+      "LANGUAGE"    => "English",
+      "LOCATION"    => "USA",
+      "MINIMUM AGE" => "18",
+      "WEBSITE"     => "beoran.net",
+      "FAMILY"      => "Custom",
+      "GENRE"       => "Science Fiction",
+      "GAMEPLAY"    => "Adventure",
+      "STATUS"      => "Alpha",
+      "GAMESYSTEM"  => "Custom",
+      "INTERMUD"    => "",
+      "SUBGENRE"    => "None",
+      "AREAS"       => "0",
+      "HELPFILES"   => "0",
+      "MOBILES"     => "0",
+      "OBJECTS"     => "0",
+      "ROOMS"       => "1",
+      "CLASSES"     => "0",
+      "LEVELS"      => "0",
+      "RACES"       => "3",
+      "SKILLS"      => "900",
+      "ANSI"        => "1",
+      "MCCP"        => "1",
+      "MCP"         => "0",
+      "MSDP"        => "0",
+      "MSP"         => "0",
+      "MXP"         => "0",
+      "PUEBLO"      => "0",
+      "UTF-8"       => "1",
+      "VT100"       => "1",
+      "XTERM 255 COLORS" => "1",
+      "PAY TO PLAY"      => "0",
+      "PAY FOR PERKS"    => "0",
+      "HIRING BUILDERS"  => "0",
+      "HIRING CODERS"    => "0" 
+    }    
   end
   
+  
+  # Returns the MSSP data
+  def mssp
+    @mssp["PLAYERS"] = @clients.size.to_s
+    return @mssp
+  end
+
+
+  
   # Look for a free numeric client ID in the client hash.
   def get_free_client_id
     cli = 0
@@ -70,7 +125,7 @@ class Server
       rsock = cl.io
       @clients.delete(id)
       @reading.delete(rsock)
-      rsock.close
+      cl.close
     end     
   end
   
@@ -115,9 +170,10 @@ class Server
             add_client
           # Kick out clients with broken connections.
           elsif rsock.eof?
-            @clients.delete(client_for_socket(rsock))
+            cli = client_for_socket(rsock)
+            @clients.delete(cli.id)
             @reading.delete(rsock)
-            rsock.close
+            cli.close
           else
             # Tell the client to get their read on.              
             client  = client_for_socket(rsock)