HiveBrain v1.2.0
Get Started
← Back to all entries
patternrubyMinor

State machine for handling Telnet sequences for an FTP server

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
handlingftptelnetserverforstatemachinesequences

Problem

I've asked Code Climate to generate metrics for the ftpd Ruby gem. It correctly identified the God class; I know what to do about that. But one of the smaller classes has me stumped. This is telnet.rb:

```
# -- ruby encoding: us-ascii --

module Ftpd

# Handle the limited processing of Telnet sequences required by the
# FTP RFCs.
#
# Telnet option processing is quite complex, but we need do only a
# simple subset of it, since we can disagree with any request by the
# client to turn on an option (RFC-1123 4.1.2.12). Adhering to
# RFC-1143 ("The Q Method of Implementing TELNET Option Negiation"),
# and supporting only what's needed to keep all options turned off:
#
# * Reply to WILL sequence with DONT sequence
# * Reply to DO sequence with WONT sequence
# * Ignore WONT sequence
# * Ignore DONT sequence
#
# We also handle the "interrupt process" and "data mark" sequences,
# which the client sends before the ABORT command, by ignoring them.
#
# All Telnet sequence start with an IAC, followed by at least one
# character. Here are the sequences we care about:
#
# SEQUENCE CODES
# ----------------- --------------------
# WILL IAC WILL option-code
# WONT IAC WONT option-code
# DO IAC DO option-code
# DONT IAC DONT option-code
# escaped 255 IAC IAC
# interrupt process IAC IP
# data mark IAC DM
#
# Any pathalogical sequence (e.g. IAC + \x01), or any sequence we
# don't recognize, we pass through.

class Telnet

# The command with recognized Telnet sequences removed

attr_reader :plain

# Any Telnet sequences to send

attr_reader :reply

# Create a new instance with a command that may contain Telnet
# sequences.
# @param command [String]

def initialize(command)
telnet_state_machine command
end

private

module Codes

Solution

As suggested by @m_x, This solution uses a parser of sorts, driven by the StringScanner built-in class. This is very compact, pretty readable, and gets rid of the state machine altogether:

Some methods to handle actions:

def accept(scanner)   
  @plain << scanner[1]
end

def reply_dont(scanner)
  @reply << IAC + DONT + scanner[1]
end

def reply_wont(scanner)
  @reply << IAC + WONT + scanner[1]
end

def ignore(scanner)
end


A list of telnet sequences:

# Telnet sequences to handle, and how to handle them

SEQUENCES = [
  [/#{IAC}(#{IAC})/, :accept],
  [/#{IAC}#{WILL}(.)/m, :reply_dont],
  [/#{IAC}#{WONT}(.)/m, :ignore],
  [/#{IAC}#{DO}(.)/m, :reply_wont],
  [/#{IAC}#{DONT}(.)/m, :ignore],
  [/#{IAC}#{IP}/, :ignore],
  [/#{IAC}#{DM}/, :ignore],
  [/(.)/m, :accept],
]


And the parser that uses them:

# Parse the the command.  Sets @plain and @reply

def parse_command(command)
  @plain = ''
  @reply = ''
  scanner = StringScanner.new(command)
  while !scanner.eos?
    SEQUENCES.each do |regexp, method|
      if scanner.scan(regexp)
        send method, scanner
        break
      end
    end
  end
end

Code Snippets

def accept(scanner)   
  @plain << scanner[1]
end

def reply_dont(scanner)
  @reply << IAC + DONT + scanner[1]
end

def reply_wont(scanner)
  @reply << IAC + WONT + scanner[1]
end

def ignore(scanner)
end
# Telnet sequences to handle, and how to handle them

SEQUENCES = [
  [/#{IAC}(#{IAC})/, :accept],
  [/#{IAC}#{WILL}(.)/m, :reply_dont],
  [/#{IAC}#{WONT}(.)/m, :ignore],
  [/#{IAC}#{DO}(.)/m, :reply_wont],
  [/#{IAC}#{DONT}(.)/m, :ignore],
  [/#{IAC}#{IP}/, :ignore],
  [/#{IAC}#{DM}/, :ignore],
  [/(.)/m, :accept],
]
# Parse the the command.  Sets @plain and @reply

def parse_command(command)
  @plain = ''
  @reply = ''
  scanner = StringScanner.new(command)
  while !scanner.eos?
    SEQUENCES.each do |regexp, method|
      if scanner.scan(regexp)
        send method, scanner
        break
      end
    end
  end
end

Context

StackExchange Code Review Q#32144, answer score: 4

Revisions (0)

No revisions yet.