patternrubyMinor
State machine for handling Telnet sequences for an FTP server
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
```
# -- 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:
A list of telnet sequences:
And the parser that uses them:
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)
endA 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
endCode 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
endContext
StackExchange Code Review Q#32144, answer score: 4
Revisions (0)
No revisions yet.