diff --git a/libraries/Terminal/Shell.cpp b/libraries/Terminal/Shell.cpp index 872c0ac4..2393b835 100644 --- a/libraries/Terminal/Shell.cpp +++ b/libraries/Terminal/Shell.cpp @@ -99,6 +99,8 @@ Shell::~Shell() * serial port or TCP network connection. * \param maxHistory The number of commands to allocate in the history * 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 * is insufficient memory for the history stack. * @@ -114,10 +116,10 @@ Shell::~Shell() * * \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. - Terminal::begin(stream); + Terminal::begin(stream, mode); // Create the history buffer. this->maxHistory = maxHistory; diff --git a/libraries/Terminal/Shell.h b/libraries/Terminal/Shell.h index 739e56c7..2186fac5 100644 --- a/libraries/Terminal/Shell.h +++ b/libraries/Terminal/Shell.h @@ -63,7 +63,7 @@ public: 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 loop(); diff --git a/libraries/Terminal/TelnetDefs.h b/libraries/Terminal/TelnetDefs.h new file mode 100644 index 00000000..64de630c --- /dev/null +++ b/libraries/Terminal/TelnetDefs.h @@ -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 diff --git a/libraries/Terminal/Terminal.cpp b/libraries/Terminal/Terminal.cpp index cc815b5c..d966868f 100644 --- a/libraries/Terminal/Terminal.cpp +++ b/libraries/Terminal/Terminal.cpp @@ -21,6 +21,7 @@ */ #include "Terminal.h" +#include "TelnetDefs.h" /** * \class Terminal Terminal.h @@ -102,6 +103,13 @@ #define STATE_ESC 2 // Last character was ESC. #define STATE_MATCH 3 // Matching an escape 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 // concluding that it is KEY_ESC rather than an escape sequence. @@ -131,6 +139,8 @@ Terminal::Terminal() , offset(0) , state(STATE_INIT) , 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. * * \param stream The underlying stream, whether a serial port, TCP connection, * 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 (RFC 854) + * 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; ucode = -1; state = STATE_INIT; + flags = 0; + mod = mode; } /** @@ -167,6 +200,13 @@ void Terminal::end() _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. * @@ -321,6 +361,10 @@ static bool escapeSequenceStart(int ch) * with unicodeKey() set to 0x0D. This ensures that all line ending * 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() */ int Terminal::readKey() @@ -378,6 +422,12 @@ int Terminal::readKey() if (ch == 0x0A) { ucode = -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. @@ -430,6 +480,9 @@ int Terminal::readKey() offset = ch & 0x07; utf8len = 4; state = STATE_UTF8; + } else if (ch == 0xFF && mod == Telnet) { + // Start of a telnet command (IAC byte). + state = STATE_IAC; } break; @@ -489,6 +542,170 @@ int Terminal::readKey() state = STATE_INIT; } 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. @@ -558,12 +775,15 @@ size_t Terminal::writeUnicode(long code) * This function should be used if the application has some information * about the actual window size. For serial ports, this usually isn't * 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 * 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) { @@ -1177,3 +1397,18 @@ int Terminal::matchEscape(int ch) } 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); +} diff --git a/libraries/Terminal/Terminal.h b/libraries/Terminal/Terminal.h index dbd009bb..d9e62448 100644 --- a/libraries/Terminal/Terminal.h +++ b/libraries/Terminal/Terminal.h @@ -30,15 +30,26 @@ // Special key code that indicates that unicodeKey() contains the actual code. #define KEY_UNICODE 0x1000 +// Special key code that indicates that the window size has changed. +#define KEY_WINSIZE 0x1001 + class Terminal : public Stream { public: Terminal(); virtual ~Terminal(); - void begin(Stream &stream); + enum Mode + { + Serial, + Telnet + }; + + void begin(Stream &stream, Mode mode = Serial); void end(); + Terminal::Mode mode() const { return (Terminal::Mode)mod; } + virtual int available(); virtual int peek(); virtual int read(); @@ -123,8 +134,12 @@ private: uint16_t offset; uint8_t state; uint8_t utf8len; + uint8_t mod; + uint8_t sb[16]; + uint8_t flags; int matchEscape(int ch); + void telnetCommand(uint8_t type, uint8_t option); }; #endif diff --git a/libraries/Terminal/examples/TelnetServer/TelnetServer.ino b/libraries/Terminal/examples/TelnetServer/TelnetServer.ino new file mode 100644 index 00000000..1330baa4 --- /dev/null +++ b/libraries/Terminal/examples/TelnetServer/TelnetServer.ino @@ -0,0 +1,87 @@ + +/* +This example demonstrates how to create a simple telnet server. + +This example is placed into the public domain. +*/ + +#include +#include +#include + +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(); +}