17 May, 2011, Twisol wrote in the 1st comment:
Votes: 0
I've been working on a custom Telnet library for a little while now, and I think it's pretty safe to say it's done now. It's called Anachronism, it's on GitHub, and I'd love to get some feedback on it. (There's also a Ruby binding!)

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.
17 May, 2011, donky wrote in the 2nd comment:
Votes: 0
Can you write some Python to demonstrate how it would be used?
17 May, 2011, Twisol wrote in the 3rd comment:
Votes: 0
I don't use Python enough to do an example justice! :sad: I do have a Ruby example though, with a working MCCP (v2) implementation. (edit: Oh, and there's no Python binding yet. I'm sure it's not too hard to do with ctypes, and you can refer to the (kind of ugly) FFI code in the Ruby binding for inspiration.)

Just FYI: I don't claim this is the best code or the right code, but it works and I believe it displays the power of the channels abstraction. You can easily build another channel (say, GMCP) and hook it into the NVT to add protocol support, without cluttering up the rest of the code horribly.

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
18 May, 2011, JohnnyStarr wrote in the 4th comment:
Votes: 0
Looks pretty useful. I'm no telnet expert, but I think you have cut out a lot of headache for anyone
that needs this in their project.
18 May, 2011, Twisol wrote in the 5th comment:
Votes: 0
Glad to hear it! :biggrin:
25 Jul, 2011, Twisol wrote in the 6th comment:
Votes: 0
Over the weekend I took some time to clean up some issues with the API. Primarily, the extra channels functions add a lot of complexity, and while I still like the idea of channels, I've replaced them with a single telnet_telopt_callback passed to telnet_nvt_new() that handles all telopt-based events (toggle, focus, and data). You can effectively route from here to more specific telopt handlers.

I've also updated the Ruby binding. I actually started from my ideal Ruby interface to Telnet and worked backwards to the C API, so this should be pretty easy to use. Here's my MCCP example ported to the new interface:

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
12 Aug, 2011, Vigud wrote in the 7th comment:
Votes: 0
I saw your Ruby binding for Anachronism today and I also know you have some experience with programming in Lua. Could you make a Lua binding for me, please?
13 Aug, 2011, Twisol wrote in the 8th comment:
Votes: 0
A Lua binding has been on my list of "Things I'd like to do" for a while, but I haven't had the time or inclination to work on it. I have Ruby and Node.js bindings, and I'm focusing my efforts on the latter while I work on Aspect.
0.0/8