2 * This file is part of lanterna (http://code.google.com/p/lanterna/).
4 * lanterna is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU Lesser General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU Lesser General Public License for more details.
14 * You should have received a copy of the GNU Lesser General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 * Copyright (C) 2010-2015 Martin
19 package com
.googlecode
.lanterna
.terminal
.ansi
;
21 import static com
.googlecode
.lanterna
.terminal
.ansi
.TelnetProtocol
.*;
22 import java
.io
.ByteArrayOutputStream
;
23 import java
.io
.IOException
;
24 import java
.io
.InputStream
;
25 import java
.io
.OutputStream
;
26 import java
.net
.Socket
;
27 import java
.net
.SocketAddress
;
28 import java
.nio
.charset
.Charset
;
29 import java
.util
.Arrays
;
30 import java
.util
.ArrayList
;
31 import java
.util
.List
;
34 * This class is used by the {@code TelnetTerminalServer} class when a client has connected in; this class will be the
35 * interaction point for that client. All operations are sent to the client over the network socket and some of the
36 * meta-operations (like echo mode) are communicated using Telnet negotiation language. You can't create objects of this
37 * class directly; they are created for you when you are listening for incoming connections using a
38 * {@code TelnetTerminalServer} and a client connects.
40 * A good resource on telnet communication is http://www.tcpipguide.com/free/t_TelnetProtocol.htm<br>
41 * Also here: http://support.microsoft.com/kb/231866
42 * @see TelnetTerminalServer
45 public class TelnetTerminal
extends ANSITerminal
{
47 private final Socket socket
;
48 private final NegotiationState negotiationState
;
50 TelnetTerminal(Socket socket
, Charset terminalCharset
) throws IOException
{
51 this(socket
, new TelnetClientIACFilterer(socket
.getInputStream()), socket
.getOutputStream(), terminalCharset
);
54 //This weird construction is just so that we can access the input filter without changing the visibility in StreamBasedTerminal
55 private TelnetTerminal(Socket socket
, TelnetClientIACFilterer inputStream
, OutputStream outputStream
, Charset terminalCharset
) throws IOException
{
56 super(inputStream
, outputStream
, terminalCharset
);
58 this.negotiationState
= inputStream
.negotiationState
;
59 inputStream
.setEventListener(new TelnetClientEventListener() {
61 public void onResize(int columns
, int rows
) {
62 TelnetTerminal
.this.onResized(columns
, rows
);
66 public void requestReply(boolean will
, byte option
) throws IOException
{
67 writeToTerminal(COMMAND_IAC
, will ? COMMAND_WILL
: COMMAND_WONT
, option
);
72 setResizeNotificationOn();
76 * Returns the socket address for the remote endpoint of the telnet connection
77 * @return SocketAddress representing the remote client
79 public SocketAddress
getRemoteSocketAddress() {
80 return socket
.getRemoteSocketAddress();
83 private void setEchoOff() throws IOException
{
84 writeToTerminal(COMMAND_IAC
, COMMAND_WILL
, OPTION_ECHO
);
88 private void setLineMode0() throws IOException
{
90 COMMAND_IAC
, COMMAND_DO
, OPTION_LINEMODE
,
91 COMMAND_IAC
, COMMAND_SUBNEGOTIATION
, OPTION_LINEMODE
, (byte)1, (byte)0, COMMAND_IAC
, COMMAND_SUBNEGOTIATION_END
);
95 private void setResizeNotificationOn() throws IOException
{
97 COMMAND_IAC
, COMMAND_DO
, OPTION_NAWS
);
102 * Retrieves the current negotiation state with the client, containing details on what options have been enabled
103 * and what the client has said it supports.
104 * @return The current negotiation state for this client
106 public NegotiationState
getNegotiationState() {
107 return negotiationState
;
111 * Closes the socket to the client, effectively ending the telnet session and the terminal.
112 * @throws IOException If there was an underlying I/O error
114 public void close() throws IOException
{
119 * This class contains some of the various states that the Telnet negotiation protocol defines. Lanterna doesn't
120 * support all of them but the more common ones are represented.
122 public static class NegotiationState
{
123 private boolean clientEcho
;
124 private boolean clientLineMode0
;
125 private boolean clientResizeNotification
;
126 private boolean suppressGoAhead
;
127 private boolean extendedAscii
;
130 this.clientEcho
= true;
131 this.clientLineMode0
= false;
132 this.clientResizeNotification
= false;
133 this.suppressGoAhead
= true;
134 this.extendedAscii
= true;
138 * Is the telnet client echo mode turned on (client is echoing characters locally)
139 * @return {@code true} if client echo is enabled
141 public boolean isClientEcho() {
146 * Is the telnet client line mode 0 turned on (client sends character by character instead of line by line)
147 * @return {@code true} if client line mode 0 is enabled
149 public boolean isClientLineMode0() {
150 return clientLineMode0
;
154 * Is the telnet client resize notification turned on (client notifies server when the terminal window has
156 * @return {@code true} if client resize notification is enabled
158 public boolean isClientResizeNotification() {
159 return clientResizeNotification
;
164 * Is the telnet client suppress go-ahead turned on
165 * @return {@code true} if client suppress go-ahead is enabled
167 public boolean isSuppressGoAhead() {
168 return suppressGoAhead
;
172 * Is the telnet client extended ascii turned on
173 * @return {@code true} if client extended ascii is enabled
175 public boolean isExtendedAscii() {
176 return extendedAscii
;
179 private void onUnsupportedStateCommand(boolean enabling
, byte value
) {
180 System
.err
.println("Unsupported operation: Client says it " + (enabling ?
"will" : "won't") + " do " + TelnetProtocol
.CODE_TO_NAME
.get(value
));
183 private void onUnsupportedRequestCommand(boolean askedToDo
, byte value
) {
184 System
.err
.println("Unsupported request: Client asks us, " + (askedToDo ?
"do" : "don't") + " " + TelnetProtocol
.CODE_TO_NAME
.get(value
));
187 private void onUnsupportedSubnegotiation(byte option
, byte[] additionalData
) {
188 System
.err
.println("Unsupported subnegotiation: Client send " + TelnetProtocol
.CODE_TO_NAME
.get(option
) + " with extra data " +
189 toList(additionalData
));
192 private static List
<String
> toList(byte[] array
) {
193 List
<String
> list
= new ArrayList
<String
>(array
.length
);
195 list
.add(String
.format("%02X ", b
));
201 private interface TelnetClientEventListener
{
202 void onResize(int columns
, int rows
);
203 void requestReply(boolean will
, byte option
) throws IOException
;
206 private static class TelnetClientIACFilterer
extends InputStream
{
207 private final NegotiationState negotiationState
;
208 private final InputStream inputStream
;
209 private final byte[] buffer
;
210 private final byte[] workingBuffer
;
211 private int bytesInBuffer
;
212 private TelnetClientEventListener eventListener
;
214 TelnetClientIACFilterer(InputStream inputStream
) {
215 this.negotiationState
= new NegotiationState();
216 this.inputStream
= inputStream
;
217 this.buffer
= new byte[64 * 1024];
218 this.workingBuffer
= new byte[1024];
219 this.bytesInBuffer
= 0;
220 this.eventListener
= null;
223 private void setEventListener(TelnetClientEventListener eventListener
) {
224 this.eventListener
= eventListener
;
228 public int read() throws IOException
{
229 throw new UnsupportedOperationException("TelnetClientIACFilterer doesn't support .read()");
233 public void close() throws IOException
{
238 public int available() throws IOException
{
239 int underlyingStreamAvailable
= inputStream
.available();
240 if(underlyingStreamAvailable
== 0 && bytesInBuffer
== 0) {
243 else if(underlyingStreamAvailable
== 0) {
244 return bytesInBuffer
;
246 else if(bytesInBuffer
== buffer
.length
) {
247 return bytesInBuffer
;
250 return bytesInBuffer
;
254 @SuppressWarnings("NullableProblems") //I can't find the correct way to fix this!
255 public int read(byte[] b
, int off
, int len
) throws IOException
{
256 if(inputStream
.available() > 0) {
259 if(bytesInBuffer
== 0) {
262 int bytesToCopy
= Math
.min(len
, bytesInBuffer
);
263 System
.arraycopy(buffer
, 0, b
, off
, bytesToCopy
);
264 System
.arraycopy(buffer
, bytesToCopy
, buffer
, 0, buffer
.length
- bytesToCopy
);
265 bytesInBuffer
-= bytesToCopy
;
269 private void fillBuffer() throws IOException
{
270 int readBytes
= inputStream
.read(workingBuffer
, 0, Math
.min(workingBuffer
.length
, buffer
.length
- bytesInBuffer
));
271 if(readBytes
== -1) {
274 for(int i
= 0; i
< readBytes
; i
++) {
275 if(workingBuffer
[i
] == COMMAND_IAC
) {
277 if(Arrays
.asList(COMMAND_DO
, COMMAND_DONT
, COMMAND_WILL
, COMMAND_WONT
).contains(workingBuffer
[i
])) {
278 parseCommand(workingBuffer
, i
, readBytes
);
282 else if(workingBuffer
[i
] == COMMAND_SUBNEGOTIATION
) { //0xFA = SB = Subnegotiation
283 i
+= parseSubNegotiation(workingBuffer
, ++i
, readBytes
);
286 else if(workingBuffer
[i
] != COMMAND_IAC
) { //Double IAC = 255
287 System
.err
.println("Unknown Telnet command: " + workingBuffer
[i
]);
290 buffer
[bytesInBuffer
++] = workingBuffer
[i
];
294 private void parseCommand(byte[] buffer
, int position
, int max
) throws IOException
{
295 if(position
+ 1 >= max
) {
296 throw new IllegalStateException("State error, we got a command signal from the remote telnet client but "
297 + "not enough characters available in the stream");
299 byte command
= buffer
[position
];
300 byte value
= buffer
[position
+ 1];
304 if(value
== OPTION_SUPPRESS_GO_AHEAD
) {
305 negotiationState
.suppressGoAhead
= (command
== COMMAND_DO
);
306 eventListener
.requestReply(command
== COMMAND_DO
, value
);
308 else if(value
== OPTION_EXTEND_ASCII
) {
309 negotiationState
.extendedAscii
= (command
== COMMAND_DO
);
310 eventListener
.requestReply(command
== COMMAND_DO
, value
);
313 negotiationState
.onUnsupportedRequestCommand(command
== COMMAND_DO
, value
);
318 if(value
== OPTION_ECHO
) {
319 negotiationState
.clientEcho
= (command
== COMMAND_WILL
);
321 else if(value
== OPTION_LINEMODE
) {
322 negotiationState
.clientLineMode0
= (command
== COMMAND_WILL
);
324 else if(value
== OPTION_NAWS
) {
325 negotiationState
.clientResizeNotification
= (command
== COMMAND_WILL
);
328 negotiationState
.onUnsupportedStateCommand(command
== COMMAND_WILL
, value
);
332 throw new UnsupportedOperationException("No command handler implemented for " + TelnetProtocol
.CODE_TO_NAME
.get(command
));
336 private int parseSubNegotiation(byte[] buffer
, int position
, int max
) {
337 int originalPosition
= position
;
340 byte operation
= buffer
[position
++];
342 //Read until [IAC SE]
343 ByteArrayOutputStream outputBuffer
= new ByteArrayOutputStream();
344 while(position
< max
) {
345 byte read
= buffer
[position
];
346 if(read
!= COMMAND_IAC
) {
347 outputBuffer
.write(read
);
350 if(position
+ 1 == max
) {
351 throw new IllegalStateException("State error, unexpected end of buffer when reading subnegotiation");
354 if(buffer
[position
] == COMMAND_IAC
) {
355 outputBuffer
.write(COMMAND_IAC
); //Escaped IAC
357 else if(buffer
[position
] == COMMAND_SUBNEGOTIATION_END
) {
358 parseSubNegotiation(operation
, outputBuffer
.toByteArray());
359 return ++position
- originalPosition
;
364 throw new IllegalStateException("State error, unexpected end of buffer when reading subnegotiation, no IAC SE");
367 private void parseSubNegotiation(byte option
, byte[] additionalData
) {
370 eventListener
.onResize(
371 convertTwoBytesToInt2(additionalData
[1], additionalData
[0]),
372 convertTwoBytesToInt2(additionalData
[3], additionalData
[2]));
374 case OPTION_LINEMODE
:
375 //We don't parse this, as this is a very complicated command :(
376 //Let's leave it for now, fingers crossed
379 negotiationState
.onUnsupportedSubnegotiation(option
, additionalData
);
385 private static int convertTwoBytesToInt2(byte b1
, byte b2
) {
386 return ( (b2
& 0xFF) << 8) | (b1
& 0xFF);