/*
 * This file is part of lanterna (http://code.google.com/p/lanterna/).
 *
 * lanterna is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Copyright (C) 2010-2020 Martin Berglund
 */
package com.googlecode.lanterna.terminal.ansi;

import static com.googlecode.lanterna.terminal.ansi.TelnetProtocol.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.SocketAddress;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;

This class is used by the TelnetTerminalServer class when a client has connected in; this class will be the interaction point for that client. All operations are sent to the client over the network socket and some of the meta-operations (like echo mode) are communicated using Telnet negotiation language. You can't create objects of this class directly; they are created for you when you are listening for incoming connections using a TelnetTerminalServer and a client connects.

A good resource on telnet communication is http://www.tcpipguide.com/free/t_TelnetProtocol.htm
Also here: http://support.microsoft.com/kb/231866

Author:martin
See Also:
/** * This class is used by the {@code TelnetTerminalServer} class when a client has connected in; this class will be the * interaction point for that client. All operations are sent to the client over the network socket and some of the * meta-operations (like echo mode) are communicated using Telnet negotiation language. You can't create objects of this * class directly; they are created for you when you are listening for incoming connections using a * {@code TelnetTerminalServer} and a client connects. * <p> * A good resource on telnet communication is http://www.tcpipguide.com/free/t_TelnetProtocol.htm<br> * Also here: http://support.microsoft.com/kb/231866 * @see TelnetTerminalServer * @author martin */
public class TelnetTerminal extends ANSITerminal { private final Socket socket; private final NegotiationState negotiationState; TelnetTerminal(Socket socket, Charset terminalCharset) throws IOException { this(socket, new TelnetClientIACFilterer(socket), socket.getOutputStream(), terminalCharset); } //This weird construction is just so that we can access the input filter without changing the visibility in StreamBasedTerminal private TelnetTerminal(Socket socket, TelnetClientIACFilterer inputStream, OutputStream outputStream, Charset terminalCharset) throws IOException { super(inputStream, outputStream, terminalCharset); this.socket = socket; this.negotiationState = inputStream.negotiationState; inputStream.setEventListener(new TelnetClientEventListener() { @Override public void onResize(int columns, int rows) { TelnetTerminal.this.onResized(columns, rows); } @Override public void requestReply(boolean will, byte option) throws IOException { writeToTerminal(COMMAND_IAC, will ? COMMAND_WILL : COMMAND_WONT, option); } }); setLineMode0(); setEchoOff(); setResizeNotificationOn(); }
Returns the socket address for the remote endpoint of the telnet connection
Returns:SocketAddress representing the remote client
/** * Returns the socket address for the remote endpoint of the telnet connection * @return SocketAddress representing the remote client */
public SocketAddress getRemoteSocketAddress() { return socket.getRemoteSocketAddress(); } private void setEchoOff() throws IOException { writeToTerminal(COMMAND_IAC, COMMAND_WILL, OPTION_ECHO); flush(); } private void setLineMode0() throws IOException { writeToTerminal( COMMAND_IAC, COMMAND_DO, OPTION_LINEMODE, COMMAND_IAC, COMMAND_SUBNEGOTIATION, OPTION_LINEMODE, (byte)1, (byte)0, COMMAND_IAC, COMMAND_SUBNEGOTIATION_END); flush(); } private void setResizeNotificationOn() throws IOException { writeToTerminal( COMMAND_IAC, COMMAND_DO, OPTION_NAWS); flush(); }
Retrieves the current negotiation state with the client, containing details on what options have been enabled and what the client has said it supports.
Returns:The current negotiation state for this client
/** * Retrieves the current negotiation state with the client, containing details on what options have been enabled * and what the client has said it supports. * @return The current negotiation state for this client */
public NegotiationState getNegotiationState() { return negotiationState; }
Closes the socket to the client, effectively ending the telnet session and the terminal.
Throws:
  • IOException – If there was an underlying I/O error
/** * Closes the socket to the client, effectively ending the telnet session and the terminal. * @throws IOException If there was an underlying I/O error */
@Override public void close() throws IOException { super.close(); socket.close(); }
This class contains some of the various states that the Telnet negotiation protocol defines. Lanterna doesn't support all of them but the more common ones are represented.
/** * This class contains some of the various states that the Telnet negotiation protocol defines. Lanterna doesn't * support all of them but the more common ones are represented. */
public static class NegotiationState { private boolean clientEcho; private boolean clientLineMode0; private boolean clientResizeNotification; private boolean suppressGoAhead; private boolean extendedAscii; NegotiationState() { this.clientEcho = true; this.clientLineMode0 = false; this.clientResizeNotification = false; this.suppressGoAhead = true; this.extendedAscii = true; }
Is the telnet client echo mode turned on (client is echoing characters locally)
Returns:true if client echo is enabled
/** * Is the telnet client echo mode turned on (client is echoing characters locally) * @return {@code true} if client echo is enabled */
public boolean isClientEcho() { return clientEcho; }
Is the telnet client line mode 0 turned on (client sends character by character instead of line by line)
Returns:true if client line mode 0 is enabled
/** * Is the telnet client line mode 0 turned on (client sends character by character instead of line by line) * @return {@code true} if client line mode 0 is enabled */
public boolean isClientLineMode0() { return clientLineMode0; }
Is the telnet client resize notification turned on (client notifies server when the terminal window has changed size)
Returns:true if client resize notification is enabled
/** * Is the telnet client resize notification turned on (client notifies server when the terminal window has * changed size) * @return {@code true} if client resize notification is enabled */
public boolean isClientResizeNotification() { return clientResizeNotification; }
Is the telnet client suppress go-ahead turned on
Returns:true if client suppress go-ahead is enabled
/** * Is the telnet client suppress go-ahead turned on * @return {@code true} if client suppress go-ahead is enabled */
public boolean isSuppressGoAhead() { return suppressGoAhead; }
Is the telnet client extended ascii turned on
Returns:true if client extended ascii is enabled
/** * Is the telnet client extended ascii turned on * @return {@code true} if client extended ascii is enabled */
public boolean isExtendedAscii() { return extendedAscii; } private void onUnsupportedStateCommand(boolean enabling, byte value) { System.err.println("Unsupported operation: Client says it " + (enabling ? "will" : "won't") + " do " + TelnetProtocol.CODE_TO_NAME.get(value)); } private void onUnsupportedRequestCommand(boolean askedToDo, byte value) { System.err.println("Unsupported request: Client asks us, " + (askedToDo ? "do" : "don't") + " " + TelnetProtocol.CODE_TO_NAME.get(value)); } private void onUnsupportedSubnegotiation(byte option, byte[] additionalData) { System.err.println("Unsupported subnegotiation: Client send " + TelnetProtocol.CODE_TO_NAME.get(option) + " with extra data " + toList(additionalData)); } private static List<String> toList(byte[] array) { List<String> list = new ArrayList<String>(array.length); for(byte b: array) { list.add(String.format("%02X ", b)); } return list; } } private interface TelnetClientEventListener { void onResize(int columns, int rows); void requestReply(boolean will, byte option) throws IOException; } private static class TelnetClientIACFilterer extends InputStream { private final NegotiationState negotiationState; private final InputStream inputStream; private final byte[] buffer; private final byte[] workingBuffer; private int bytesInBuffer; private TelnetClientEventListener eventListener; private Socket socket; TelnetClientIACFilterer(Socket socket) throws IOException { this.negotiationState = new NegotiationState(); this.inputStream = socket.getInputStream(); this.buffer = new byte[64 * 1024]; this.workingBuffer = new byte[1024]; this.bytesInBuffer = 0; this.eventListener = null; this.socket = socket; } private void setEventListener(TelnetClientEventListener eventListener) { this.eventListener = eventListener; } @Override public int read() throws IOException { throw new UnsupportedOperationException("TelnetClientIACFilterer doesn't support .read()"); } @Override public void close() throws IOException { inputStream.close(); } @Override public int available() throws IOException { if(bytesInBuffer > 0) { return bytesInBuffer; } fillBuffer(false); return Math.abs(bytesInBuffer); } @Override @SuppressWarnings("NullableProblems") //I can't find the correct way to fix this! public int read(byte[] b, int off, int len) throws IOException { if (bytesInBuffer == -1) { return -1; } if(available() == 0) { // There was nothing in the buffer and the underlying // stream has nothing available, so do a blocking read // from the stream. fillBuffer(true); } if(bytesInBuffer <= 0) { return -1; } int bytesToCopy = Math.min(len, bytesInBuffer); System.arraycopy(buffer, 0, b, off, bytesToCopy); System.arraycopy(buffer, bytesToCopy, buffer, 0, buffer.length - bytesToCopy); bytesInBuffer -= bytesToCopy; return bytesToCopy; } private void fillBuffer(boolean block) throws IOException { int maxFill = Math.min(workingBuffer.length, buffer.length - bytesInBuffer); int oldTimeout = socket.getSoTimeout(); if (!block) { socket.setSoTimeout(1); } int readBytes = inputStream.read(workingBuffer, 0, maxFill); if (!block) { socket.setSoTimeout(oldTimeout); } if (readBytes == -1) { bytesInBuffer = -1; return; } for(int i = 0; i < readBytes; i++) { if(workingBuffer[i] == COMMAND_IAC) { i++; if(Arrays.asList(COMMAND_DO, COMMAND_DONT, COMMAND_WILL, COMMAND_WONT).contains(workingBuffer[i])) { parseCommand(workingBuffer, i, readBytes); ++i; continue; } else if(workingBuffer[i] == COMMAND_SUBNEGOTIATION) { //0xFA = SB = Subnegotiation i += parseSubNegotiation(workingBuffer, ++i, readBytes); continue; } else if(workingBuffer[i] != COMMAND_IAC) { //Double IAC = 255 System.err.println("Unknown Telnet command: " + workingBuffer[i]); } } buffer[bytesInBuffer++] = workingBuffer[i]; } } private void parseCommand(byte[] buffer, int position, int max) throws IOException { if(position + 1 >= max) { throw new IllegalStateException("State error, we got a command signal from the remote telnet client but " + "not enough characters available in the stream"); } byte command = buffer[position]; byte value = buffer[position + 1]; switch(command) { case COMMAND_DO: case COMMAND_DONT: if(value == OPTION_SUPPRESS_GO_AHEAD) { negotiationState.suppressGoAhead = (command == COMMAND_DO); eventListener.requestReply(command == COMMAND_DO, value); } else if(value == OPTION_EXTEND_ASCII) { negotiationState.extendedAscii = (command == COMMAND_DO); eventListener.requestReply(command == COMMAND_DO, value); } else { negotiationState.onUnsupportedRequestCommand(command == COMMAND_DO, value); } break; case COMMAND_WILL: case COMMAND_WONT: if(value == OPTION_ECHO) { negotiationState.clientEcho = (command == COMMAND_WILL); } else if(value == OPTION_LINEMODE) { negotiationState.clientLineMode0 = (command == COMMAND_WILL); } else if(value == OPTION_NAWS) { negotiationState.clientResizeNotification = (command == COMMAND_WILL); } else { negotiationState.onUnsupportedStateCommand(command == COMMAND_WILL, value); } break; default: throw new UnsupportedOperationException("No command handler implemented for " + TelnetProtocol.CODE_TO_NAME.get(command)); } } private int parseSubNegotiation(byte[] buffer, int position, int max) { int originalPosition = position; //Read operation byte operation = buffer[position++]; //Read until [IAC SE] ByteArrayOutputStream outputBuffer = new ByteArrayOutputStream(); while(position < max) { byte read = buffer[position]; if(read != COMMAND_IAC) { outputBuffer.write(read); } else { if(position + 1 == max) { throw new IllegalStateException("State error, unexpected end of buffer when reading subnegotiation"); } position++; if(buffer[position] == COMMAND_IAC) { outputBuffer.write(COMMAND_IAC); //Escaped IAC } else if(buffer[position] == COMMAND_SUBNEGOTIATION_END) { parseSubNegotiation(operation, outputBuffer.toByteArray()); return ++position - originalPosition; } } position++; } throw new IllegalStateException("State error, unexpected end of buffer when reading subnegotiation, no IAC SE"); } private void parseSubNegotiation(byte option, byte[] additionalData) { switch(option) { case OPTION_NAWS: eventListener.onResize( convertTwoBytesToInt2(additionalData[1], additionalData[0]), convertTwoBytesToInt2(additionalData[3], additionalData[2])); break; case OPTION_LINEMODE: //We don't parse this, as this is a very complicated command :( //Let's leave it for now, fingers crossed break; default: negotiationState.onUnsupportedSubnegotiation(option, additionalData); break; } } } private static int convertTwoBytesToInt2(byte b1, byte b2) { return ( (b2 & 0xFF) << 8) | (b1 & 0xFF); } }