package jackpal.androidterm.sample.telnet; import java.io.InputStream; import java.io.OutputStream; import java.util.Arrays; import android.util.Log; import jackpal.androidterm.emulatorview.TermSession; /** * A rudimentary Telnet client implemented as a subclass of TermSession. * * Telnet, as specified in RFC 854, is a fairly simple protocol: for the * most part, we send and receive streams of bytes which can be fed directly * into the terminal emulator. However, there are a handful of complications: * * - The standard says that CR (ASCII carriage return) must either be * translated into the network standard newline sequence CR LF, or be * followed immediately by NUL. * - (byte) 255, called IAC in the standard, introduces Telnet command * sequences which can be used to negotiate options, perform certain * actions on the "Network Virtual Terminal" which the standard defines, * or do flow control. * - By default, the protocol spoken is designed to accommodate a half-duplex * terminal on either end, so we should be able to buffer output and * send it on a trigger (the sequence IAC GA). * - By default, we're expected to be able to echo local keyboard input into * our own output. * * To solve these problems, we filter the input from the network to catch * and implement Telnet commands via the processInput() method. Similarly, we * filter the output from TermSession by overriding write() to modify CR as * required by the standard, and pass a buffer with manually controllable * flushing to the TermSession to use as its output stream. * * In addition to the base Telnet protocol, we implement two options: * the ECHO option (RFC 857) for enabling echoing of input across the network, * and the SUPPRESS-GO-AHEAD option (RFC 858) for omitting half-duplex flow * control. Both of these are commonly available from servers, and make our * lives easier. */ public class TelnetSession extends TermSession { private static final String TAG = "TelnetSession"; private static final boolean DEBUG = false; public static final int IAC = 255; public static final int CMD_SE = 240; // SE -- end of parameters public static final int CMD_NOP = 241; // NOP public static final int CMD_MARK = 242; // data mark public static final int CMD_BRK = 243; // send BREAK to terminal public static final int CMD_IP = 244; // Interrupt Process public static final int CMD_AO = 245; // Abort Output public static final int CMD_AYT = 246; // Are You There public static final int CMD_EC = 247; // Erase Character public static final int CMD_EL = 248; // Erase Line public static final int CMD_GA = 249; // Go Ahead (clear to send) public static final int CMD_SB = 250; // SB -- begin parameters public static final int CMD_WILL = 251; // used in option negotiation public static final int CMD_WONT = 252; // used in option negotiation public static final int CMD_DO = 253; // used in option negotiation public static final int CMD_DONT = 254; // used in option negotiation public static final int OPTION_ECHO = 1; // see RFC 857 public static final int OPTION_SUPPRESS_GO_AHEAD = 3; // see RFC 858 public static final int OPTION_RANDOMLY_LOSE = 256; // see RFC 748 :) // Whether we believe the remote end implements the telnet protocol private boolean peerIsTelnetd = false; private boolean peerEchoesInput = false; /* RFC 854 says false is the default, but that makes the client effectively useless for connecting to random non-Telnet servers for debugging */ private boolean peerSuppressedGoAhead = true; private boolean echoInput = false; /* RFC 854 says false is the default, but that makes the client effectively useless for connecting to random non-Telnet servers for debugging */ private boolean suppressGoAhead = true; private boolean doSuppressGaRequested = false; private boolean willSuppressGaRequested = false; /* Telnet command processor state */ private boolean mInTelnetCommand = false; private int mTelnetCommand = 0; private boolean mMultipleParameters = false; private int mLastInputByteProcessed = 0; /** * Create a TelnetSession to handle the telnet protocol and terminal * emulation, using an existing InputStream and OutputStream. */ public TelnetSession(InputStream termIn, OutputStream termOut) { setTermIn(termIn); setTermOut(termOut); } /** * Process data before sending it to the server. * We replace all occurrences of \r with \r\n, as required by the * Telnet protocol (CR meant to be a newline should be sent as CR LF, * and all other CRs must be sent as CR NUL). */ @Override public void write(byte[] bytes, int offset, int count) { // Count the number of CRs int numCRs = 0; for (int i = offset; i < offset + count; ++i) { if (bytes[i] == '\r') { ++numCRs; } } if (numCRs == 0) { // No CRs -- just send data as-is doWrite(bytes, offset, count); if (isRunning() && !peerEchoesInput) { doLocalEcho(bytes); } return; } // Convert CRs into CRLFs byte[] translated = new byte[count + numCRs]; int j = 0; for (int i = offset; i < offset + count; ++i) { if (bytes[i] == '\r') { translated[j++] = '\r'; translated[j++] = '\n'; } else { translated[j++] = bytes[i]; } } // Send the data doWrite(translated, 0, translated.length); // If server echo is off, echo the entered characters locally if (isRunning() && !peerEchoesInput) { doLocalEcho(translated); } } private byte[] mWriteBuf = new byte[4096]; private int mWriteBufLen = 0; /* Send data to the server, buffering it first if necessary */ private void doWrite(byte[] data, int offset, int count) { if (peerSuppressedGoAhead) { // No need to buffer -- send it straight to the server super.write(data, offset, count); return; } /* Flush the buffer if it's full ... not strictly correct, but better than the alternatives */ byte[] buffer = mWriteBuf; int bufLen = mWriteBufLen; if (bufLen + count > buffer.length) { flushWriteBuf(); bufLen = 0; } // Queue the data to be sent at the next server GA System.arraycopy(data, offset, buffer, bufLen, count); mWriteBufLen += count; } /* Flush the buffer of data to be written to the server */ private void flushWriteBuf() { super.write(mWriteBuf, 0, mWriteBufLen); mWriteBufLen = 0; } /* Echoes local input from the emulator back to the emulator screen. */ private void doLocalEcho(byte[] data) { if (DEBUG) { Log.d(TAG, "echoing " + Arrays.toString(data) + " back to terminal"); } appendToEmulator(data, 0, data.length); notifyUpdate(); } /** * Input filter which handles Telnet commands and copies data to the * terminal emulator. */ @Override public void processInput(byte[] buffer, int offset, int count) { int lastByte = mLastInputByteProcessed; for (int i = offset; i < offset + count; ++i) { // need to interpret the byte as unsigned -- thanks Java! int curByte = ((int) buffer[i]) & 0xff; if (DEBUG) { Log.d(TAG, "input byte " + curByte); } if (mInTelnetCommand) { // Previous byte was part of a command sequence doTelnetCommand(curByte); lastByte = curByte; continue; } switch (curByte) { case IAC: // Telnet command prefix mInTelnetCommand = true; /* Assume we're talking to a real Telnet server */ if (!peerIsTelnetd) { doTelnetInit(); } break; case CMD_GA: // GA -- clear to send /** * If we're in half-duplex flow control, we've been given * permission to send data; flush our output buffers. * * Note that it's not strictly correct to send the other * side a GA at this point, but since we're not actually * attached to a half-duplex terminal, we don't have a signal * to indicate when the other side should logically begin * to send again. * * In full-duplex operation (option SUPPRESS-GO-AHEAD enabled), * does nothing. */ byte[] cmdGa = { (byte) IAC, (byte) CMD_GA }; if (!peerSuppressedGoAhead) { if (!suppressGoAhead) { doWrite(cmdGa, 0, cmdGa.length); } flushWriteBuf(); } break; case 0: // NUL -- should be ignored following a CR if (lastByte == '\r') { if (echoInput) { // We do need to echo it back to the server, though doEchoInput(0); } break; } default: /* Send the data to the terminal emulator, and echo it back across the network if the other end wants us to do so. */ super.processInput(buffer, i, 1); if (echoInput) { doEchoInput(buffer[i]); } } lastByte = curByte; } // Save the last byte processed -- we may need it mLastInputByteProcessed = lastByte; } byte[] mOneByte = new byte[1]; private void doEchoInput(int input) { if (DEBUG) { Log.d(TAG, "echoing " + input + " to remote end"); } byte[] oneByte = mOneByte; oneByte[0] = (byte) input; super.write(oneByte, 0, 1); } /** * Interpreter for Telnet commands. */ private void doTelnetCommand(int curByte) { /* Handle parameter lists */ if (mMultipleParameters) { switch (curByte) { case CMD_SE: // SE -- end of parameters doMultiParamCommand(); finishTelnetCommand(); return; default: addMultiParam(curByte); return; } } /* Handle option negotiation */ switch (mTelnetCommand) { case CMD_WILL: handleWillOption(curByte); return; case CMD_WONT: handleWontOption(curByte); return; case CMD_DO: handleDoOption(curByte); return; case CMD_DONT: handleDontOption(curByte); return; } /* Telnet commands */ switch (curByte) { case CMD_EC: // EC -- erase character // ESC [ D (VT100 cursor left) byte[] cmdLeft = { (byte) 27, (byte) '[', (byte) 'D' }; // ESC [ P (VT100 erase char at cursor) byte[] cmdErase = { (byte) 27, (byte) '[', (byte) 'P' }; super.processInput(cmdLeft, 0, cmdLeft.length); super.processInput(cmdErase, 0, cmdErase.length); break; case CMD_EL: // EL -- erase line // ESC [ 2 K (VT100 clear whole line) byte[] cmdEl = { (byte) 27, (byte) '[', (byte) '2', (byte) 'K' }; super.processInput(cmdEl, 0, cmdEl.length); break; case IAC: // send the IAC character to terminal byte[] iac = { (byte) IAC }; super.processInput(iac, 0, iac.length); break; case CMD_SB: // SB -- more parameters follow option mMultipleParameters = true; return; case CMD_WILL: // WILL case CMD_WONT: // WON'T case CMD_DO: // DO case CMD_DONT: // DON'T // Option negotiation -- save the command and wait for the option mTelnetCommand = curByte; return; case CMD_AYT: // AYT -- Are You There /** * RFC 854 says we should send back "some visible (i.e., printable) * evidence that the AYT was received" ... this is as good as * anything else */ byte[] msg = "yes, I'm here\r\n".getBytes(); super.write(msg, 0, msg.length); break; // The following are unimplemented case CMD_MARK: // data mark case CMD_BRK: // send a break to the terminal case CMD_IP: // IP -- interrupt process case CMD_AO: // AO -- abort output case CMD_NOP: // NOP default: break; } finishTelnetCommand(); } // end of command, process next byte normally private void finishTelnetCommand() { mTelnetCommand = 0; mInTelnetCommand = false; mMultipleParameters = false; } private void addMultiParam(int curByte) { // unimplemented } private void doMultiParamCommand() { // unimplemented } /** * Telnet option negotiation code. * * Because the Telnet protocol is defined to be mostly symmetric with * respect to the client and server roles, option negotiation can be * somewhat confusing. The same commands are used to initiate and * respond to negotiation requests, and their exact meaning depends on * whether they were sent as an initial request or as a response: * * - WILL: If sent as a request, indicates that we wish to enable the * option on our end. If sent as a response, indicates that we * have enabled the specified option on our end. * - WON'T: If sent as a request, indicates that we insist on disabling the * option on our end. If sent as a response, indicates that we * refuse to enable the specified option on our end. * - DO: If sent as a request, indicates that we wish the peer to enable * this option on the remote end. If sent as a response, indicates * that we accept the peer's request to enable the option on the * remote end. * - DON'T: If sent as a request, indicates that we demand the peer disable * this option on the remote end. If sent as a response, indicates * that we refuse to allow the peer to enable this option on the * remote end. * * All options are off by default (options have to be explicitly requested). * In order to prevent negotiation loops, we are not supposed to reply to * requests which do not change the state of an option (e.g. if the server * sends DON'T ECHO and we're not echoing back what the server sends us, we * should not reply with WON'T ECHO). * * Examples: * * - server sends WILL ECHO, we reply DO ECHO: the server asks, and we * agree, that the server echo the input we send to it back to us over * the network. * - we send WON'T ECHO, server replies DON'T ECHO: we ask, and the server * agrees, that we not echo the input we receive from the server back to * the server over the network. * - we send DO SUPPRESS-GO-AHEAD, server replies WILL SUPPRESS-GO-AHEAD: * we ask, and the server agrees, that the server not send GA to indicate * when it's ready to take data (in other words, we can freely send data * to the server). * - server sends DO ECHO, we reply WON'T ECHO: the server asks us to * echo the input we receive from it back over the network, but we refuse * to do so. */ private void handleWillOption(int curByte) { switch (curByte) { case OPTION_ECHO: // WILL ECHO // We don't ever request DO ECHO, so this must be a request if (!peerEchoesInput) { sendOption(CMD_DO, OPTION_ECHO); } peerEchoesInput = true; break; case OPTION_SUPPRESS_GO_AHEAD: // WILL SUPPRESS-GO-AHEAD if (!doSuppressGaRequested && !peerSuppressedGoAhead) { // This is a request which changes our state, send a reply sendOption(CMD_DO, OPTION_SUPPRESS_GO_AHEAD); } peerSuppressedGoAhead = true; doSuppressGaRequested = false; // Flush unwritten data in the output buffer flushWriteBuf(); break; default: // unrecognized option // refuse to let other end enable unknown options sendOption(CMD_DONT, curByte); break; } finishTelnetCommand(); } private void handleWontOption(int curByte) { switch (curByte) { case OPTION_ECHO: // WON'T ECHO // We don't ever request DO ECHO, so this must be a request if (peerEchoesInput) { sendOption(CMD_DONT, OPTION_ECHO); } peerEchoesInput = false; break; case OPTION_SUPPRESS_GO_AHEAD: // WON'T SUPPRESS-GO-AHEAD if (!doSuppressGaRequested && peerSuppressedGoAhead) { // This is a request which changes our state, send a reply sendOption(CMD_DONT, OPTION_SUPPRESS_GO_AHEAD); } peerSuppressedGoAhead = false; doSuppressGaRequested = false; break; default: // unrecognized option // WON'T is the default for any option, so we shouldn't reply break; } finishTelnetCommand(); } private void handleDoOption(int curByte) { switch (curByte) { case OPTION_ECHO: // DO ECHO /* Other Telnet clients like netkit-telnet refuse this request when they receive it, since it doesn't make much sense */ sendOption(CMD_WONT, OPTION_ECHO); /** // We don't ever request WILL ECHO, so this must be a request if (!echoInput) { sendOption(CMD_WILL, OPTION_ECHO); } echoInput = true; */ break; case OPTION_SUPPRESS_GO_AHEAD: // DO SUPPRESS-GO-AHEAD if (!willSuppressGaRequested && !suppressGoAhead) { // This is a request which changes our state, send a reply sendOption(CMD_WILL, OPTION_SUPPRESS_GO_AHEAD); } suppressGoAhead = true; willSuppressGaRequested = false; break; default: // unrecognized option // refuse to enable unknown options sendOption(CMD_WONT, curByte); break; } finishTelnetCommand(); } private void handleDontOption(int curByte) { switch (curByte) { case OPTION_ECHO: // DON'T ECHO // We don't ever request DON'T ECHO, so this must be a request if (echoInput) { sendOption(CMD_WONT, OPTION_ECHO); } echoInput = false; break; case OPTION_SUPPRESS_GO_AHEAD: // DON'T SUPPRESS-GO-AHEAD if (!willSuppressGaRequested && suppressGoAhead) { // This is a request which changes our state, send a reply sendOption(CMD_WONT, curByte); } suppressGoAhead = false; willSuppressGaRequested = false; break; default: // unrecognized option // DON'T is the default for any option, so we shouldn't reply break; } finishTelnetCommand(); } /* Send an option negotiation command */ private void sendOption(int command, int opt) { if (DEBUG) { Log.d(TAG, "sending command: " + command + " " + opt); } // option negotiation needs to bypass the write buffer byte[] buffer = { (byte) IAC, (byte) command, (byte) opt }; super.write(buffer, 0, buffer.length); } private void requestDoSuppressGoAhead() { doSuppressGaRequested = true; // send IAC DO SUPPRESS-GO-AHEAD sendOption(CMD_DO, OPTION_SUPPRESS_GO_AHEAD); } private void requestWillSuppressGoAhead() { willSuppressGaRequested = true; // send IAC WILL SUPPRESS-GO-AHEAD sendOption(CMD_WILL, OPTION_SUPPRESS_GO_AHEAD); } /** * Called the first time processInput() encounters IAC in the input stream, * which is a reasonably good heuristic to determine that the other end is * a true Telnet server and not some SMTP/POP/IMAP/whatever server. * * When called, disables the SUPPRESS-GO-AHEAD option for both directions * (required by the standard, but very inconvenient when talking to * non-Telnet servers) and sends requests to reenable it in both directions * (because it's much easier for us when it's on). */ private void doTelnetInit() { peerSuppressedGoAhead = false; suppressGoAhead = false; requestDoSuppressGoAhead(); requestWillSuppressGoAhead(); peerIsTelnetd = true; } }