Telnet++
A C++ library for interacting with Telnet streams
|
Telnet++ is an implementation of the Telnet Session Layer protocol that is used primarily to negotiate a feature set between a client and server, the former of which is usually some kind of text-based terminal, Commonly used terminals include Xterm, PuTTY, and a whole host of Telnet-enabled MUD clients including Tintin++, MushClient, and more.
Telnet++ requires a C++14 compiler and the Boost Libraries. It also uses Google Test for its testing suite, which is compiled optionally.
Telnet++ is automatically tested with Clang and G++ 5.2.
For further information about the working status of Telnet++, to report any bugs, or to make any feature requests, visit the Waffle board
The protocol has three basic elements, all of which are accessed by using the 0xFF character called "Interpret As Command", or IAC.
Without needing to negotiate any capabilities, Telnet offers some out-of-the-box commands. These include Are You There, which is usually sent by the client to provoke a response from an otherwise-busy server; Erase Line, which could be used in interative applications to cancel a set of input, and several other commands used for negotiations between options.
Commands are represented by the telnetpp::command class.
The Telnet protocol describes a model whereby the client and server maintain separate lists of features, called "options", which can be enabled or disabled by the remote side. Individual options may each be described as "server" or "client" options, and server and client options may be mixed on each side of the connection. It is even possible in some cases that both sides of the connection can be both client and server for the same option. These options are negotiated by using the commands DO, DONT, WILL and WONT.
The various Telnet option specifications are not consistent in what is considered a server and what is considered a client, but for the purposes of this library, the server is considered as the side of the connection that does the thing, and the client is the side of the connection that wants the thing. That is, the server reacts to DO and DONT and sends WILL and WONT, and the client reacts to WILL and WONT and sends DO and DONT.
Negotiations are represented by the telnetpp::negotiation class.
After an option has been negotiated, a new channel opens up to be able to communicate in an option-specific way to the remote terminal. These are called subnegotiations, and each protocol defines its own sub-protocol. For example, the NAWS (Negotiate About Window Size) sends five bytes when reporting window size, the first of which represents an "IS" token, and the following four bytes represent two two-byte pairs that are the window extends.
Subnegotiations are represented by the telnetpp::subnegotiation class.
A telnetpp::element is a Boost.Variant that may contain a command, a negotiation, a subnegotiation, or just a plain text string representing non-Telnet-specific input/output.
Furthermore, it is also possible to send objects of any other type using Boost.Any. The main structure used to pass information around the library is therefore the telnetpp::token, which is a Boost.Variant that may contain either a Boost.Any or a telnetpp::element.
When communicating with the lower byte-streaming layer, telnetpp::elements are transformed into byte streams, which are represented by the u8stream type.
After a telnetpp::token passes through the Telnet layer, this results in a telnetpp::stream_token, which is a Boost.Variant of a telnetpp::u8stream and a Boost.Any. This enables layers above Telnet to talk to layers below Telnet.
Finally, when the telnetpp::stream_token has passed through any lower layers, any Boost.Any instances remaining can be filtered out and converted into a telnetpp::u8stream, which can be sent across the data channel proper.
The Telnet++ library does not impose any requirement on any kind of data stream API. In order to accomplish this, most parts of the API do not actually send any data; rather, they return the data to be sent, usually in the form of either collections of tokens, or as streams (described above).
As alluded to earlier, each distinct feature is represented by either a telnetpp::client_option or a telnetpp::server_option. These both enjoy the same API; they only differ in the underlying protocol. The user needs to know little about which actual negotiations and commands are sent. There are two key functions and one signal for the option classes:
All of the above can be quite complicated to manage, so Telnet++ provides the telnetpp::session class. This can be used to manage an entire Telnet feature set for a connection. This is accomplished by simply "install"ing handlers for commands and options:
After this is done, the session can be used to both receive data from lower layers and to send information from higher layers. Incoming data received in bytes is arranged into a u8stream. This can be passed into the session's receive() function. This converts the bytes into Telnet tokens, and these are routed appropriately to whichever settings are installed.
The result of this operation is a collection of tokens that represent the responses from the settings. As mentioned above, these tokens may also include user-defined datastructures, as long as they are wrapped in a boost::any. This can be used to communicate from layers above the Telnet session to the layers below.
The response of the session's receive() function should be sent immediately to the session's send() function. This function transforms the Telnet structures in the response into a byte stream. Any boost::any objects remain as they are unchanged. This can then be passed onto the lower layer.
Finally, a session's send() function can also be used to communicate directly with options. For example,
The code above: the session set-up, and the send/receive functions, are all that's necessary to use the Telnet++ library. Further sample usage can be found in Paradice9 (see https://github.com/KazDragon/paradice9/blob/master/paradice/connection.cpp – in particular, just search for the lines containing the telnet_session_ variable.)
The Mud Client Compression Protocol v2 (http://tintin.sourceforge.net/mccp/) is a unique protocol in that activating it also changes the way the Telnet Protocol is carried, since it too is compressed. This requires careful design in order to ensure that uncompressed data is not sent or received when compressed data has been sent or received, and vice versa.
The following is a rough guide to how to integrate the MCCP functionality. How it actually integrates with your application will vary. Consider functions starting with "app" to be something in your application above the Telnet layer that receives user input, and those starting with "my" to be an interaction between the Telnet layer and a lower layer that deals with sending and receiving byte streams, such as a socket API.
Unit testing is implemented with Google Test, but this requires a separate compilation. The easiest way I've found of doing this is roughly equivalent to this script (which is mirrored in .travis.yml):
sudo apt-get install libgtest-dev mkdir gtest cd gtest cmake /usr/src/gtest && make export GTEST_ROOT=$PWD
After this, re-running cmake with Telnet++ should pick up both the Google Test includes in /usr/include and also the binary which was just built. If GTest was not found, Telnet++ will still build; just the automatic tests will be elided.