require "anachronism"
require "zlib"
class MccpChannel < Anachronism::Channel
# Called when the channel is negotiated open.
def on_open (where)
case where
when :local
@out = Zlib::Deflate.new
send ''
end
end
# Called when the channel is closed.
def on_close (where)
case where
when :remote
@in = nil
when :local
@out = nil
end
end
# Called on IAC SB <option>.
def on_focus
end
# Called on IAC SE.
def on_blur
@in = Zlib::Inflate.new
nvt.interrupt(option, 0)
end
# Called with data from the subnegotiation as it's received.
def on_data (data)
# MCCP doesn't -have- any data, just empty messages.
end
# Called by you!
def inflate (data)
return data unless @in
data = @in.inflate(data)
if @in.finished?
# There may be some uncompressed data after the compressed stream ends,
# so get that out too.
data << @in.flush_next_out
@in = nil
end
data
end
# Called by you!
def deflate (data)
return data unless @out
@out.deflate(data, Zlib::SYNC_FLUSH)
end
end
# The primary channel, i.e. stuff outside subnegotiations.
# It doesn't receive open/close events since it's always open.
# It also doesn't receive focus/blur events, though that's
# more subjective…
class MainChannel < Anachronism::Channel
def on_data (data)
print(data)
end
end
# The NVT is what you pass incoming data to, and register channels with.
# I'm subclassing in this example.
class MudNVT < Anachronism::NVT
def initialize (sock)
super()
@sock = sock
main = MainChannel.new
main.register(self, :main)
@mccp = MccpChannel.new
# :local => false basically means WONT.
# :remote => :lazy means DO, but only if they send WILL first.
@mccp.register(self, 86, :local => :false, :remote => :lazy)
# This usage is basically what a client would use.
# The MCCP implementation supports both ways, though,
# so it's just a matter of registering :local => true and
# using @mccp.deflate to use it in a server.
end
# Overriding the superclass's receive method here. It's just an example
# - in production code I'd use a straight NVT instance and put the
# channels/decompressing in another class.
def receive (data)
data = @mccp.inflate(data)
while true
bytes_used = super(data)
break unless bytes_used < data.length
data = data[bytes_used..-1]
# Checks if the MCCP channel interrupted the parser.
if last_interrupt_code == [86, 0]
data = @mccp.inflate(data)
end
end
end
# Called by Anachronism when there's data to send.
def on_send (data)
@sock.send_data(data)
end
end
require "anachronism"
require "zlib"
class MccpChannel < Anachronism::Channel
def begin_compression
raise "MCCP must be activated before compression can be enabled." unless local_enabled?
raise "Compression is already enabled." if compress?
send ''
@out = Zlib::Deflate.new
end
def finish_compression
raise "Compression is not enabled." unless compress?
telnet.send_text @out.flush(Zlib::FINISH)
@out = nil
end
def inflate (data)
data = @in.inflate(data)
if @in.finished?
# There may be some uncompressed data after the compressed stream ends,
# so get that out too.
data << @in.flush_next_out
@in = nil
end
data
end
def deflate (data)
@out.deflate(data, Zlib::SYNC_FLUSH)
end
def inflate?
!!@in
end
def deflate?
!!@out
end
#
# Callbacks
##
def on_local_toggle (active)
if active
begin_compression
else
finish_compression
end
end
def on_remote_toggle (active)
if !active
@in = nil
end
end
def on_blur
@in = Zlib::Inflate.new
telnet.interrupt_parser
end
end
class MudNVT < Anachronism::Telnet
def initialize (server, client)
@server = server
@client = client
@mccp = MccpChannel.new(86, self)
@mccp.request_remote_enable :lazy => true
end
def receive (data)
total_length = data.length
while data.length > 0
data = @mccp.inflate(data) if @mccp.inflate?
data = super(data)
end
total_length
end
def on_send (data)
data = @mccp.deflate(data) if @mccp.deflate?
@server << data
end
def on_text (text)
@client << text
end
end
Anachronism allows you to consider Telnet as a set of data channels, rather than a stream of text with embedded data. Telopts are treated almost like ports, and when you attach a channel to one, Anachronism handles negotiation automatically and presents subnegotiation data as just another stream of data. This makes it really easy to keep Telnet away from the part of your code that actually does stuff, and it makes it possible to create a channel implementation once and share it with everyone.
It's also possible to drop down to a lower level at any time if you need to (and you can even avoid channels entirely). For example, lets say we're implementing an MCCP channel. You might get some compressed data in the same packet as the IAC SB COMPRESSv2 IAC SE sequence, but once you give it to the Telnet parser, how do you stop and decompress? You can interrupt the parser and it'll report back the amount of the data it processed, so you can take the rest and decompress it.
Or lets say you want to implement MCCP1 compatibility. You can catch the IAC SB event at a lower level and interrupt the parser, allowing you to go back and find the SE, fit an IAC in before it, and pass the result back in. :devil:
You might be aware of Elanthis' libtelnet library. There's nothing wrong with it per se, but I opted to write my own because (1) it has too many telopts built in and (2) it has no convenient mechanism for handling telopts. I also like to think Anachronism is cleaner, but you can be the judge of that. Still, the seeds of Anachronism came from Elanthis' past posts on telnet as a state machine, so credit where credit's due!
If you plan on looking at the code, for the love of all that is holy, src/parser.c is generated. I already had someone freak out about the gotos. I use Ragel to process the incoming data, and it generates the parser.c file from parser_common.rl (the grammar itself) and parser.rl (the actions and exposed API). The rest of Anachronism is in src/nvt.c, which is all C.
If you'd like to see the parser's state graph, you can find that here.
So… yeah. Like I said, I'd be really interested in feedback. I spent a decent amount of time on this thing, and I'm pretty happy with how it's turned out. I'm already using this to implement Aspect's telnet interface, too.