I'm extremely proud to announce that I've finished my Telnet library (written in C), Anachronism! It's fresh out of the oven, so it probably contains a number of bugs, but I'm stamping those out as I find them. While I designed it with MUDs in mind, Anachronism should be suitable for any Telnet-based application.
Github: https://github.com/Twisol/anachronism
Ruby binding: https://github.com/Twisol/anachronism-rb
(Node.js binding in the works.)
Anachronism is comprised of three primary components: the parser, the NVT, and the channels. The closest to the metal is the parser, which translates a Telnet stream into a stream of events.
The NVT provides a convenient interface for you to produce an outgoing Telnet stream. This includes functions such as telnet_send_option() and telnet_send_subnegotiation_start(). The NVT also exposes a parser (via telnet_recv()), so you only need to deal with a single object.
The most unique part of Anachronism are the channels. At this level, Telnet is treated as a data multiplexer, with 256 channels (one for each option) plus the main channel. Channels may be created and registered with an option on an NVT, and events will be routed to the channel. Through this mechanism, each Telnet subprotocol (such as MCCP, GMCP, et cetera) can be implemented in isolation, resulting in cleaner and more reusable code.
If anyone wants to use Anachronism and/or has any questions/concerns/criticism, please let me know! I'm using this for my in-progress MUD client, but it's meant to be used by anyone, so I'm open to comments.
Here's an example program showing Anachronism at work. (Note that while this uses the GMCP option code 201, it's not really GMCP. It's just an example.)
#include <string.h>
#include <stdio.h>
#include <anachronism/nvt.h>
typedef struct gmcp_data
{
telnet_channel* channel;
} gmcp_data;
// This is called when the option is negotiated on (DO/WILL) or off (DONT/WONT).
void on_gmcp_toggle(telnet_channel* channel,
telnet_channel_mode on,
telnet_channel_provider who)
{
// Get the gmcp_data object back from the channel.
if (on)
{
if (who == TELNET_CHANNEL_LOCAL)
{
const telnet_byte* data = "Foo.Bar [4, 5, 6]";
telnet_channel_send(channel, data, strlen(data));
}
}
}
// This is called for IAC SB <option>, IAC SE, and any data between the two.
void on_gmcp_data(telnet_channel* channel,
telnet_channel_event type,
const telnet_byte* data,
size_t length)
{
// Get the gmcp_data object back from the channel.
gmcp_data* gmcp = NULL;
telnet_channel_get_userdata(channel, (void**)&gmcp);
switch (type)
{
case TELNET_CHANNEL_EV_BEGIN:
printf("<GMCP>\n");
break;
case TELNET_CHANNEL_EV_END:
printf("</GMCP>\n");
break;
case TELNET_CHANNEL_EV_DATA:
printf("[GMCP] %.*s\n", length, data);
break;
}
}
gmcp_data* gmcp_new(telnet_channel** out)
{
gmcp_data* gmcp = malloc(sizeof(gmcp_data));
if (gmcp)
{
// Create a new channel, hooking it up to our two GMCP event handlers.
// We store the gmcp_data object with it so we have a context
// in the event handlers.
telnet_channel* channel = telnet_channel_new(&on_gmcp_toggle,
&on_gmcp_data,
(void*)gmcp);
if (channel)
{
gmcp->channel = channel;
*out = channel;
}
else
{
free(gmcp);
gmcp = NULL;
}
}
return gmcp;
}
void on_event(telnet_nvt* nvt, telnet_event* event)
{
switch (event->type)
{
// Command, warning, and send events aren't routed to channels.
// The others can be processed here as well, but it's not very useful
// except for logging.
case TELNET_EV_COMMAND:
break;
case TELNET_EV_WARNING:
break;
// A send event is emitted when the NVT has data to send to the remote host.
case TELNET_EV_SEND:
{
telnet_send_event* ev = (telnet_send_event*)event;
printf("[OUT] %.*s\n", ev->length, ev->data);
break;
}
default:
// everything else is handled by channels, so we ignore it
break;
}
}
int main()
{
// Create an NVT, routing events to the on_event method.
// We're not storing any extra data with this NVT.
telnet_nvt* nvt = telnet_nvt_new(&on_event, NULL);
// Create a Telnet channel for GMCP.
telnet_channel* channel = NULL;
gmcp_data* gmcp = gmcp_new(&channel);
// Bind the GMCP channel to option 201.
// The two final parameters are comparable to WILL/WONT and DO/DONT.
// In this case, we WILL offer GMCP, but we DONT want the client to offer it.
//
// TELNET_CHANNEL_LAZY is also available. In this mode, we only accept
// if the remote host asks first. This is useful for clients, as
// some MUDs don't understand Telnet very well. It's best to wait until
// the server offers a channel before accepting it.
telnet_channel_register(channel, nvt, 201, TELNET_CHANNEL_ON, TELNET_CHANNEL_OFF);
// Process some "incoming" data.
const char* data = "\xFF\xFD\xC9" // IAC DO GMCP
"\xFF\xFA\xC9" // IAC SB GMCP
"Foo.Bar [1, 2, 3]" // data
"\xFF\xF0"; // IAC SE
size_t bytes_used;
telnet_recv(nvt, (const telnet_byte*)data, strlen(data), &bytes_used);
return 0;
}
|