fsxNet Wiki

BBS Development & Resources

User Tools

Site Tools


tutorials:crystal_bbs:part_two

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revision Previous revision
Next revision
Previous revision
tutorials:crystal_bbs:part_two [2017/04/09 14:58]
sardaukar
tutorials:crystal_bbs:part_two [2018/03/29 01:58] (current)
Line 7: Line 7:
 The plan for the tutorial at this stage is to handle some feature negotiation in this article, and the remaining negotiations and input on the next one. Haven'​t decided what to do after that, but probably a state machine interaction post or two, and then you should know most of what it takes to build a basic old-school BBS system. So, on to feature negotiation. The plan for the tutorial at this stage is to handle some feature negotiation in this article, and the remaining negotiations and input on the next one. Haven'​t decided what to do after that, but probably a state machine interaction post or two, and then you should know most of what it takes to build a basic old-school BBS system. So, on to feature negotiation.
  
-I've been using [[https://​syncterm.bbsdev.net/​|SyncTerm]] as my terminal problem for these posts. When connecting to the prototype BBS, I noticed the feature negotiation flow using the "​toggle options"​ telnet client trick from the [[tutorials:​crystal_bbs:​part_zero|intro post]]. We'll focus on those being requested, and not the whole spectrum of options ​available. IANA has [[http://​www.iana.org/​assignments/​telnet-options/​telnet-options.xhtml|a list]] of possible ​options.+I've been using [[https://​syncterm.bbsdev.net/​|SyncTerm]] as my terminal problem for these posts. When connecting to the prototype BBS we're building, I noticed the feature negotiation flow using the "​toggle options"​ telnet client trick from the [[tutorials:​crystal_bbs:​part_zero|intro post]]. We'll focus on a subset of options, and not the whole spectrum of possible ​options. IANA has [[http://​www.iana.org/​assignments/​telnet-options/​telnet-options.xhtml|a list]] of all Telnet ​options. After implementing these, others should hopefully be easy to implement as well.
  
 Our prototype will: Our prototype will:
Line 76: Line 76:
 module BBS module BBS
   IAC  = 255   IAC  = 255
-  ​DONT = 254+  ​
   DO   = 253   DO   = 253
-  ​WONT 252+  ​DONT 254 
 +  ​
   WILL = 251   WILL = 251
 +  WONT = 252
 + 
   SB   = 250   SB   = 250
   SE   = 240   SE   = 240
Line 87: Line 90:
   SUP_GOAHEAD ​  ​= ​ 3   SUP_GOAHEAD ​  ​= ​ 3
   TERMINAL_TYPE = 24   TERMINAL_TYPE = 24
-  NAWS          = 31 
 end end
 </​code>​ </​code>​
Line 141: Line 143:
 </​code>​ </​code>​
  
-First off, we write the slice (byte array) passed onto the socket. This will signal the client we want to initiate the negotiation for the feature encoded on the array. Then, we read the reply. The reply is always three bytes long, we so create a ''​reply''​ slice and read the response onto it from the socket. Then we get a bit lazy and only check the middle byte in the reply! Since the first byte will be IAC, and the last one is a repetition of the value we got, we only care if the middle byte is DO/WILL or DONT/​WONT. ​ Beware that in a proper system, we'd need some error handling here. Based on the yes/no character of the reply, we set the key passed on the ''​conn''​ hash as true or false.+First off, we write the slice (byte array) passed onto the socket. This will signal the client we want to initiate the negotiation for the feature encoded on the array. Then, we read the reply. The reply is always three bytes long, we so create a ''​reply''​ slice and read the response onto it from the socket. Then we get a bit lazy and only check the middle byte in the reply! Since the first byte will be IAC, and the last one is a repetition of the value we sent, we only care if the middle byte is DO/WILL or DONT/​WONT. ​ Beware that in a proper system, we'd need some error handling here. Based on the yes/no character of the reply, we set the key passed on the ''​conn''​ hash as true or false.
  
 Thus, after this method runs, the hash will hold if the client agreed to local echo or not! Let's add the remaining options, and print out a summary at the end: Thus, after this method runs, the hash will hold if the client agreed to local echo or not! Let's add the remaining options, and print out a summary at the end:
Line 169: Line 171:
 </​code>​ </​code>​
  
-Great success! The client has agreed to all of our feature requests. There'​s one more request, but it requires special treatment. We want to get the "​terminal type" which is a string that the terminal sends to identify itself. Of course, the reply being a string, it won't be a standard IAC reply. The format of these special requests is:+Great success! The client has agreed to all of our feature requests. There'​s one more request, but it requires special treatment. We want to get the "​terminal type" which is a string that the terminal sends to identify itself. Of course, the reply being a string, it won't be a standard IAC 3-byte ​reply. The format of these special requests is:
  
   * 1st byte - 0xff / IAC (as usual)   * 1st byte - 0xff / IAC (as usual)
Line 177: Line 179:
   * last byte - 0xf0 / SE (special end)   * last byte - 0xf0 / SE (special end)
  
-So, to make the client tell us its name, we'll send ''​TERMINAL_TYPE''​ ''​ECHO''​ from out constants (making the meat of the request ''​0x18''''​0x01'''​). The response to this request will be:+So, to make the client tell us its name, we'll send ''​TERMINAL_TYPE'' ​ ''​ECHO''​ from our constants (making the meat of the request ''​0x18''​ ''​0x01'''​). The response to this request will be:
  
   * 1st byte - IAC   * 1st byte - IAC
Line 226: Line 228:
 </​code>​ </​code>​
  
-So we start off by sending the IAC extended request for the terminal type, using the ''​iac_sb''​ utility method. Then we skip the first four bytes (since we know they will be IAC/​SB/​TERMINAL_TYPE/​IS) and start reading the string sent. +So we start off by sending the IAC extended request for the terminal type, using the ''​iac_sb''​ utility method. Then we skip the first four bytes (since we know they will be IAC / SB / TERMINAL_TYPE / IS) and start reading the string sent. 
  
 We break if the byte being read is IAC and the next one is SE - we could just break on IAC, but by reading the next byte from the socket, we "​empty"​ the bytes present on the socket'​s buffer. If the byte is not IAC, we add its ANSI value (using ''​.chr''​) to the terminal string, which is then returned. We break if the byte being read is IAC and the next one is SE - we could just break on IAC, but by reading the next byte from the socket, we "​empty"​ the bytes present on the socket'​s buffer. If the byte is not IAC, we add its ANSI value (using ''​.chr''​) to the terminal string, which is then returned.
Line 254: Line 256:
  
 In the next part, we'll handle responding to features being requested by the client, and actual input. Stay tuned, see you next time! In the next part, we'll handle responding to features being requested by the client, and actual input. Stay tuned, see you next time!
 +
 +Here's the full listing of our ''​main.cr''​ file so far:
 +
 +<code ruby>
 +require "​yaml"​
 +require "​socket"​
 +
 +require "​./​aliases"​
 +require "​./​constants"​
 +
 +module BBS
 +  class Config
 +    YAML.mapping(
 +      settings: Hash(String,​ Int32)
 +    )
 +  end
 +
 +  class Main
 +    @config : Config
 +
 +    def initialize
 +      @config = load_config
 +    end
 +
 +    def go!
 +      Signal::​INT.trap { puts "​SIGINT,​ exiting gracefully!";​ exit 0 }
 +
 +      telnet_port = @config.settings["​port"​]
 +
 +      server = TCPServer.new("​localhost",​ telnet_port)
 +      puts "now listening in port #​{telnet_port}"​
 +      loop do
 +        if socket = server.accept?​
 +          spawn handle_connection(socket)
 +        end
 +      end
 +    end
 +
 +    private def iac(*args)
 +      Slice(UInt8).new(args.size + 1) do |i|
 +        i == 0 ? IAC.to_u8 : args[i - 1].to_u8
 +      end
 +    end
 +
 +    private def iac_sb(*args)
 +      Slice(UInt8).new(args.size + 4).tap do |slice|
 +        slice[0] = IAC.to_u8
 +        slice[1] = SB.to_u8
 +        args.each_with_index do |arg, idx|
 +          slice[idx + 2] = arg.to_u8
 +        end
 +        slice[-2] = IAC.to_u8
 +        slice[-1] = SE.to_u8
 +      end
 +    end
 +
 +    private def negotiate(slice,​ key, socket, conn)
 +      socket.write slice
 +
 +      reply = Slice(UInt8).new(3)
 +      socket.read_fully reply
 +
 +      case reply[1]
 +      when DO, WILL
 +        conn[key] = true
 +      when DONT, WONT
 +        conn[key] = false
 +      else
 +        puts "error negotiating #​{key}!/#​{reply.inspect}"​
 +      end
 +    end
 +
 +    private def get_terminal_string(socket)
 +      socket.write iac_sb(TERMINAL_TYPE,​ ECHO)
 +
 +      4.times { socket.read_byte }
 +
 +      term_string = ""​
 +      loop do
 +        this_byte = socket.read_byte
 +        if this_byte
 +          break if this_byte == IAC && socket.read_byte == SE
 +          term_string += this_byte.chr
 +        end
 +      end
 +
 +      term_string
 +    end
 +
 +    private def get_terminal_size(socket,​ conn)
 +      socket.write iac(DO, NAWS)
 +
 +      reply = Slice(UInt8).new(9)
 +      socket.read_fully reply
 +
 +      puts reply.inspect
 +
 +      cols = reply[4]
 +      lines = reply[6]
 +
 +      puts "NAWS -> cols: #{cols} lines: #​{lines}"​
 +
 +      conn[:​term_width] = cols.to_i
 +      conn[:​term_height] = lines.to_i
 +    end
 +
 +    private def handle_connection(socket)
 +      socket << "​detecting client features..."​
 +
 +      conn = ConnHash.new
 +
 +      negotiate(iac(WILL,​ ECHO), :echo, socket, conn)
 +      negotiate(iac(WILL,​ SUP_GOAHEAD),​ :​suppress_goahead,​ socket, conn)
 +      negotiate(iac(DO,​ BINARY), :binary, socket, conn)
 +
 +      negotiate(iac(DO,​ TERMINAL_TYPE),​ :​terminal_type,​ socket, conn)
 +      conn[:​term_string] = get_terminal_string(socket)
 +
 +      get_terminal_size(socket,​ conn)
 +
 +      puts "​features negotiated: #​{conn}"​
 +
 +      socket.close
 +    end
 +
 +    private def load_config
 +      Config.from_yaml(File.read("​config.yml"​))
 +    end
 +  end
 +end
 +</​code>​
tutorials/crystal_bbs/part_two.1491749931.txt.gz · Last modified: 2018/03/29 01:58 (external edit)