1
0
mirror of https://github.com/taigrr/arduinolibs synced 2025-01-18 04:33:12 -08:00

Telnet mode for the Terminal class

This commit is contained in:
Rhys Weatherley 2016-03-09 17:56:22 +10:00
parent ef532b3eef
commit 25aeeb0383
6 changed files with 453 additions and 8 deletions

View File

@ -99,6 +99,8 @@ Shell::~Shell()
* serial port or TCP network connection. * serial port or TCP network connection.
* \param maxHistory The number of commands to allocate in the history * \param maxHistory The number of commands to allocate in the history
* stack for scrolling back through using Up/Down arrow keys. * stack for scrolling back through using Up/Down arrow keys.
* \param mode The terminal mode to operate in, Terminal::Serial or
* Terminal::Telnet.
* \return Returns true if the shell was initialized, or false if there * \return Returns true if the shell was initialized, or false if there
* is insufficient memory for the history stack. * is insufficient memory for the history stack.
* *
@ -114,10 +116,10 @@ Shell::~Shell()
* *
* \sa end(), setPrompt() * \sa end(), setPrompt()
*/ */
bool Shell::begin(Stream &stream, size_t maxHistory) bool Shell::begin(Stream &stream, size_t maxHistory, Terminal::Mode mode)
{ {
// Initialize the Terminal base class with the underlying stream. // Initialize the Terminal base class with the underlying stream.
Terminal::begin(stream); Terminal::begin(stream, mode);
// Create the history buffer. // Create the history buffer.
this->maxHistory = maxHistory; this->maxHistory = maxHistory;

View File

@ -63,7 +63,7 @@ public:
Shell(); Shell();
virtual ~Shell(); virtual ~Shell();
bool begin(Stream &stream, size_t maxHistory = 0); bool begin(Stream &stream, size_t maxHistory = 0, Terminal::Mode mode = Serial);
void end(); void end();
void loop(); void loop();

View File

@ -0,0 +1,106 @@
/*
* Copyright (C) 2016 Southern Storm Software, Pty Ltd.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
#ifndef TELNET_DEFS_h
#define TELNET_DEFS_h
// References:
// https://tools.ietf.org/html/rfc854
// http://www.iana.org/assignments/telnet-options/telnet-options.xhtml
namespace TelnetDefs
{
/** Telnet commands */
enum Command
{
EndOfFile = 236, /**< EOF */
Suspend = 237, /**< Suspend process */
Abort = 238, /**< Abort process */
EndOfRecord = 239, /**< End of record command */
SubEnd = 240, /**< End option sub-negotiation */
NOP = 241, /**< No operation */
DataMark = 242, /**< Data mark */
Break = 243, /**< Break */
Interrupt = 244, /**< Interrupt process */
AbortOutput = 245, /**< Abort output */
AreYouThere = 246, /**< Are you there? */
EraseChar = 247, /**< Erase character */
EraseLine = 248, /**< Erase line */
GoAhead = 249, /**< Go ahead in half-duplex mode */
SubStart = 250, /**< Option sub-negotiation */
WILL = 251, /**< Will use option */
WONT = 252, /**< Won't use option */
DO = 253, /**< Do use option */
DONT = 254, /**< Don't use option */
IAC = 255 /**< Interpret As Command */
};
/** Telnet options used in sub-negotiations */
enum Option
{
Binary = 0, /**< Binary transmission */
Echo = 1, /**< Echo */
Reconnection = 2, /**< Reconnection */
SuppressGoAhead = 3, /**< Suppress half-duplex go ahead signals */
ApproxMsgSize = 4, /**< Approx message size negotiation */
Status = 5, /**< Give status on prevailing options */
TimingMark = 6, /**< Timing mark */
RemoteTransmitEcho = 7, /**< Remote controlled transmit and echo */
LineWidth = 8, /**< Line width */
PageSize = 9, /**< Page size */
CarriageReturn = 10, /**< Carriage return disposition */
HorzTabStops = 11, /**< Horizontal tab stops */
HorzTabStopDisp = 12, /**< Horizontal tab stop disposition */
FormFeed = 13, /**< Form feed disposition */
VertTabStops = 14, /**< Vertical tab stops */
VertTabStopDisp = 15, /**< Vertical tab stop disposition */
LineFeed = 16, /**< Line feed disposition */
ExtendedASCII = 17, /**< Extended ASCII */
Logout = 18, /**< Force logout */
ByteMacro = 19, /**< Byte macro */
DataEntryTerminal = 20, /**< Data entry terminal */
SUPDUP = 21, /**< SUPDUP protocol */
SUPDUPOutput = 22, /**< SUPDUP output */
SendLocation = 23, /**< Send the user's location */
TerminalType = 24, /**< Terminal type */
EndOfRecordOption = 25, /**< End of record option */
TACACSUserId = 26, /**< TACACS user identification */
OutputMarking = 27, /**< Output marking */
TerminalLocation = 28, /**< Terminal location number */
Telnet3270Regime = 29, /**< Telnet 3270 regime */
X3Pad = 30, /**< X.3 PAD */
WindowSize = 31, /**< Window size */
Speed = 32, /**< Terminal speed */
RemoteFlowControl = 33, /**< Remote flow control */
Linemode = 34, /**< Linemode option */
XDisplay = 35, /**< X display location */
EnvironmentOld = 36, /**< Environment variables (old version) */
Authentication = 37, /**< Authentication */
Encryption = 38, /**< Encryption */
Environment = 39, /**< Environment variables (new version) */
Extended = 255 /**< Extended options list */
};
};
#endif

View File

@ -21,6 +21,7 @@
*/ */
#include "Terminal.h" #include "Terminal.h"
#include "TelnetDefs.h"
/** /**
* \class Terminal Terminal.h <Terminal.h> * \class Terminal Terminal.h <Terminal.h>
@ -102,6 +103,13 @@
#define STATE_ESC 2 // Last character was ESC. #define STATE_ESC 2 // Last character was ESC.
#define STATE_MATCH 3 // Matching an escape sequence. #define STATE_MATCH 3 // Matching an escape sequence.
#define STATE_UTF8 4 // Recognizing a UTF-8 sequence. #define STATE_UTF8 4 // Recognizing a UTF-8 sequence.
#define STATE_IAC 5 // Recognizing telnet command after IAC (0xFF).
#define STATE_WILL 6 // Waiting for option code for WILL command.
#define STATE_WONT 7 // Waiting for option code for WONT command.
#define STATE_DO 8 // Waiting for option code for DO command.
#define STATE_DONT 9 // Waiting for option code for DONT command.
#define STATE_SB 10 // Option sub-negotiation.
#define STATE_SB_IAC 11 // Option sub-negotiation, byte after IAC.
// Number of milliseconds to wait after an ESC character before // Number of milliseconds to wait after an ESC character before
// concluding that it is KEY_ESC rather than an escape sequence. // concluding that it is KEY_ESC rather than an escape sequence.
@ -131,6 +139,8 @@ Terminal::Terminal()
, offset(0) , offset(0)
, state(STATE_INIT) , state(STATE_INIT)
, utf8len(0) , utf8len(0)
, mod(Terminal::Serial)
, flags(0)
{ {
} }
@ -141,19 +151,42 @@ Terminal::~Terminal()
{ {
} }
/**
* \enum Terminal::Mode
* \brief Mode to operate in, Serial or Telnet.
*/
/**
* \var Terminal::Serial
* \brief Operates the terminal in serial mode.
*/
/**
* \var Terminal::Telnet
* \brief Operates the terminal in telnet mode.
*/
/** /**
* \brief Begins terminal operations on an underlying stream. * \brief Begins terminal operations on an underlying stream.
* *
* \param stream The underlying stream, whether a serial port, TCP connection, * \param stream The underlying stream, whether a serial port, TCP connection,
* or some other stream. * or some other stream.
* \param mode The mode to operate in, either Serial or Telnet.
* *
* \sa end() * If Telnet mode is selected, then embedded commands and options from the
* telnet protocol (<a href="https://tools.ietf.org/html/rfc854">RFC 854</a>)
* will be interpreted. This is useful if the underlying \a stream is a TCP
* connection on port 23. The mode operates as a telnet server.
*
* \sa end(), mode()
*/ */
void Terminal::begin(Stream &stream) void Terminal::begin(Stream &stream, Mode mode)
{ {
_stream = &stream; _stream = &stream;
ucode = -1; ucode = -1;
state = STATE_INIT; state = STATE_INIT;
flags = 0;
mod = mode;
} }
/** /**
@ -167,6 +200,13 @@ void Terminal::end()
_stream = 0; _stream = 0;
} }
/**
* \fn Terminal::Mode Terminal::mode() const
* \brief Returns the mode this terminal is operating in, Serial or Telnet.
*
* \sa begin()
*/
/** /**
* \brief Returns the number of bytes that are available for reading. * \brief Returns the number of bytes that are available for reading.
* *
@ -321,6 +361,10 @@ static bool escapeSequenceStart(int ch)
* with unicodeKey() set to 0x0D. This ensures that all line ending * with unicodeKey() set to 0x0D. This ensures that all line ending
* types are mapped to a single KEY_RETURN report. * types are mapped to a single KEY_RETURN report.
* *
* If the window size has changed due to a remote event, then KEY_WINSIZE
* will be returned. This can allow the caller to clear and redraw the
* window in the new size.
*
* \sa unicodeKey(), read() * \sa unicodeKey(), read()
*/ */
int Terminal::readKey() int Terminal::readKey()
@ -378,6 +422,12 @@ int Terminal::readKey()
if (ch == 0x0A) { if (ch == 0x0A) {
ucode = -1; ucode = -1;
return -1; return -1;
} else if (ch == 0x00 && mod == Telnet) {
// In telnet mode, CR NUL is a literal carriage return,
// separate from the newline sequence CRLF. Eat the NUL.
// We already reported KEY_RETURN for the CR character.
ucode = -1;
return -1;
} }
// Fall through to the next case. // Fall through to the next case.
@ -430,6 +480,9 @@ int Terminal::readKey()
offset = ch & 0x07; offset = ch & 0x07;
utf8len = 4; utf8len = 4;
state = STATE_UTF8; state = STATE_UTF8;
} else if (ch == 0xFF && mod == Telnet) {
// Start of a telnet command (IAC byte).
state = STATE_IAC;
} }
break; break;
@ -489,6 +542,170 @@ int Terminal::readKey()
state = STATE_INIT; state = STATE_INIT;
} }
break; break;
case STATE_IAC:
// Telnet command byte just after an IAC (0xFF) character.
switch (ch) {
case TelnetDefs::EndOfFile:
// Convert EOF into CTRL-D.
state = STATE_INIT;
ucode = 0x04;
return 0x04;
case TelnetDefs::EndOfRecord:
// Convert end of record markers into CR.
state = STATE_INIT;
ucode = 0x0D;
return KEY_RETURN;
case TelnetDefs::Interrupt:
// Convert interrupt into CTRL-C.
state = STATE_INIT;
ucode = 0x03;
return 0x03;
case TelnetDefs::EraseChar:
// Convert erase character into DEL.
state = STATE_INIT;
ucode = 0x7F;
return KEY_BACKSPACE;
case TelnetDefs::EraseLine:
// Convert erase line into CTRL-U.
state = STATE_INIT;
ucode = 0x15;
return 0x15;
case TelnetDefs::SubStart:
// Option sub-negotiation.
utf8len = 0;
state = STATE_SB;
break;
case TelnetDefs::WILL:
// Option negotiation, WILL command.
state = STATE_WILL;
break;
case TelnetDefs::WONT:
// Option negotiation, WONT command.
state = STATE_WONT;
break;
case TelnetDefs::DO:
// Option negotiation, DO command.
state = STATE_DO;
break;
case TelnetDefs::DONT:
// Option negotiation, DONT command.
state = STATE_DONT;
break;
case TelnetDefs::IAC:
// IAC followed by IAC is the literal byte 0xFF,
// but that isn't valid UTF-8 so we just drop it.
state = STATE_INIT;
break;
default:
// Everything else is treated as a NOP.
state = STATE_INIT;
break;
}
break;
case STATE_WILL:
// Telnet option negotiation, WILL command. Note: We don't do any
// loop detection. We assume that the client will eventually break
// the loop as it probably has more memory than us to store state.
if (ch == TelnetDefs::WindowSize ||
ch == TelnetDefs::RemoteFlowControl) {
// Send a DO command in response - we accept this option.
telnetCommand(TelnetDefs::DO, ch);
} else {
// Send a DONT command in response - we don't accept this option.
telnetCommand(TelnetDefs::DONT, ch);
}
if (!(flags & 0x01)) {
// The first time we see a WILL command from the client we
// send a request back saying that we will handle echoing.
flags |= 0x01;
telnetCommand(TelnetDefs::WILL, TelnetDefs::Echo);
}
state = STATE_INIT;
break;
case STATE_WONT:
case STATE_DONT:
// Telnet option negotiation, WONT/DONT command. The other side
// is telling us that it does not understand this option or wants
// us to stop using it. For now there is nothing to do.
state = STATE_INIT;
break;
case STATE_DO:
// Telnet option negotiation, DO command. Note: Other than Echo
// we don't do any loop detection. We assume that the client will
// break the loop as it probably has more memory than us to store state.
if (ch == TelnetDefs::Echo) {
// Special handling needed for Echo - don't say WILL again
// when the client acknowledges us with a DO command.
} else if (ch == TelnetDefs::SuppressGoAhead) {
// Send a WILL command in response - we accept this option.
telnetCommand(TelnetDefs::WILL, ch);
} else {
// Send a WONT command in response - we don't accept this option.
telnetCommand(TelnetDefs::WONT, ch);
}
state = STATE_INIT;
break;
case STATE_SB:
// Telnet option sub-negotiation. Collect up all bytes and
// then execute the option once "IAC SubEnd" is seen.
if (ch == TelnetDefs::IAC) {
// IAC byte, which will be followed by either IAC or SubEnd.
state = STATE_SB_IAC;
break;
}
if (utf8len < sizeof(sb))
sb[utf8len++] = 0xFF;
break;
case STATE_SB_IAC:
// Telnet option sub-negotiation, byte after IAC.
if (ch == TelnetDefs::IAC) {
// Two IAC bytes in a row is a single escaped 0xFF byte.
if (utf8len < sizeof(sb))
sb[utf8len++] = 0xFF;
state = STATE_SB;
break;
} else if (ch == TelnetDefs::SubEnd) {
// End of the sub-negotiation field. Handle window size changes.
if (utf8len >= 5 && sb[0] == TelnetDefs::WindowSize) {
int width = (((int)(sb[1])) << 8) | sb[2];
int height = (((int)(sb[3])) << 8) | sb[4];
if (!width) // Zero width or height means "unspecified".
width = ncols;
if (!height)
height = nrows;
// Filter out obviously bogus values.
if (width >= 1 && height >= 1 && width <= 10000 && height <= 10000) {
if (width != ncols || height != nrows) {
// The window size has changed; notify the caller.
ncols = width;
nrows = height;
ucode = -1;
state = STATE_INIT;
return KEY_WINSIZE;
}
}
}
}
state = STATE_INIT;
break;
} }
// If we get here, then we're still waiting for a full sequence. // If we get here, then we're still waiting for a full sequence.
@ -558,12 +775,15 @@ size_t Terminal::writeUnicode(long code)
* This function should be used if the application has some information * This function should be used if the application has some information
* about the actual window size. For serial ports, this usually isn't * about the actual window size. For serial ports, this usually isn't
* available but telnet and ssh sessions can get the window size from * available but telnet and ssh sessions can get the window size from
* the remote host and set it using this function. * the remote host.
* *
* The window size defaults to 80x24 which is the standard default for * The window size defaults to 80x24 which is the standard default for
* terminal programs like PuTTY that emulate a VT100. * terminal programs like PuTTY that emulate a VT100.
* *
* \sa columns(), rows() * If the window size changes due to a remote event, readKey() will
* return KEY_WINSIZE to inform the application.
*
* \sa columns(), rows(), readKey()
*/ */
void Terminal::setWindowSize(int columns, int rows) void Terminal::setWindowSize(int columns, int rows)
{ {
@ -1177,3 +1397,18 @@ int Terminal::matchEscape(int ch)
} }
return -2; return -2;
} }
/**
* \brief Sends a telnet command to the client.
*
* \param type The type of command: WILL, WONT, DO, or DONT.
* \param option The telnet option the command applies to.
*/
void Terminal::telnetCommand(uint8_t type, uint8_t option)
{
uint8_t buf[3];
buf[0] = (uint8_t)TelnetDefs::IAC;
buf[1] = type;
buf[2] = option;
_stream->write(buf, 3);
}

View File

@ -30,15 +30,26 @@
// Special key code that indicates that unicodeKey() contains the actual code. // Special key code that indicates that unicodeKey() contains the actual code.
#define KEY_UNICODE 0x1000 #define KEY_UNICODE 0x1000
// Special key code that indicates that the window size has changed.
#define KEY_WINSIZE 0x1001
class Terminal : public Stream class Terminal : public Stream
{ {
public: public:
Terminal(); Terminal();
virtual ~Terminal(); virtual ~Terminal();
void begin(Stream &stream); enum Mode
{
Serial,
Telnet
};
void begin(Stream &stream, Mode mode = Serial);
void end(); void end();
Terminal::Mode mode() const { return (Terminal::Mode)mod; }
virtual int available(); virtual int available();
virtual int peek(); virtual int peek();
virtual int read(); virtual int read();
@ -123,8 +134,12 @@ private:
uint16_t offset; uint16_t offset;
uint8_t state; uint8_t state;
uint8_t utf8len; uint8_t utf8len;
uint8_t mod;
uint8_t sb[16];
uint8_t flags;
int matchEscape(int ch); int matchEscape(int ch);
void telnetCommand(uint8_t type, uint8_t option);
}; };
#endif #endif

View File

@ -0,0 +1,87 @@
/*
This example demonstrates how to create a simple telnet server.
This example is placed into the public domain.
*/
#include <SPI.h>
#include <Ethernet.h>
#include <Shell.h>
byte macAddress[6] = {
0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED
};
int ledPin = 13;
EthernetServer server(23);
EthernetClient client;
bool haveClient = false;
Shell shell;
void cmdLed(Shell &shell, int argc, char *argv[])
{
if (argc > 1 && !strcmp(argv[1], "on"))
digitalWrite(ledPin, HIGH);
else
digitalWrite(ledPin, LOW);
}
void cmdExit(Shell &shell, int argc, char *argv[])
{
client.stop();
}
ShellCommand(led, "Turns the status LED on or off", cmdLed);
ShellCommand(exit, "Exit and log out", cmdExit);
void setup()
{
// Configure I/O.
pinMode(ledPin, OUTPUT);
digitalWrite(ledPin, LOW);
// Start the serial port for status messages.
Serial.begin(9600);
Serial.println();
Serial.print("Acquiring IP address ... ");
// Start Ethernet running and get an IP address via DHCP.
if (Ethernet.begin(macAddress))
Serial.println(Ethernet.localIP());
else
Serial.println("failed");
// Listen on port 23 for incoming telnet connections.
server.begin();
// Configure the shell. We call Shell::begin() once we have a connection.
shell.setPrompt("$ ");
}
void loop()
{
// Maintain the DHCP lease over time.
Ethernet.maintain();
// Handle new/disconnecting clients.
if (!haveClient) {
// Check for new client connections.
client = server.available();
if (client) {
haveClient = true;
shell.begin(client, 5, Terminal::Telnet);
}
} else if (!client.connected()) {
// The current client has been disconnected. Shut down the shell.
shell.end();
client.stop();
client = EthernetClient();
haveClient = false;
}
// Perform periodic shell processing on the active client.
shell.loop();
}