Change build scripts
[jvcard.git] / src / com / googlecode / lanterna / terminal / ansi / TelnetTerminal.java
CommitLineData
a3b510ab
NR
1/*
2 * This file is part of lanterna (http://code.google.com/p/lanterna/).
3 *
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.
8 *
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.
13 *
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/>.
16 *
17 * Copyright (C) 2010-2015 Martin
18 */
19package com.googlecode.lanterna.terminal.ansi;
20
21import static com.googlecode.lanterna.terminal.ansi.TelnetProtocol.*;
22import java.io.ByteArrayOutputStream;
23import java.io.IOException;
24import java.io.InputStream;
25import java.io.OutputStream;
26import java.net.Socket;
27import java.net.SocketAddress;
28import java.nio.charset.Charset;
29import java.util.Arrays;
30import java.util.ArrayList;
31import java.util.List;
32
33/**
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.
39 * <p>
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
43 * @author martin
44 */
45public class TelnetTerminal extends ANSITerminal {
46
47 private final Socket socket;
48 private final NegotiationState negotiationState;
49
50 TelnetTerminal(Socket socket, Charset terminalCharset) throws IOException {
51 this(socket, new TelnetClientIACFilterer(socket.getInputStream()), socket.getOutputStream(), terminalCharset);
52 }
53
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);
57 this.socket = socket;
58 this.negotiationState = inputStream.negotiationState;
59 inputStream.setEventListener(new TelnetClientEventListener() {
60 @Override
61 public void onResize(int columns, int rows) {
62 TelnetTerminal.this.onResized(columns, rows);
63 }
64
65 @Override
66 public void requestReply(boolean will, byte option) throws IOException {
67 writeToTerminal(COMMAND_IAC, will ? COMMAND_WILL : COMMAND_WONT, option);
68 }
69 });
70 setLineMode0();
71 setEchoOff();
72 setResizeNotificationOn();
73 }
74
75 /**
76 * Returns the socket address for the remote endpoint of the telnet connection
77 * @return SocketAddress representing the remote client
78 */
79 public SocketAddress getRemoteSocketAddress() {
80 return socket.getRemoteSocketAddress();
81 }
82
83 private void setEchoOff() throws IOException {
84 writeToTerminal(COMMAND_IAC, COMMAND_WILL, OPTION_ECHO);
85 flush();
86 }
87
88 private void setLineMode0() throws IOException {
89 writeToTerminal(
90 COMMAND_IAC, COMMAND_DO, OPTION_LINEMODE,
91 COMMAND_IAC, COMMAND_SUBNEGOTIATION, OPTION_LINEMODE, (byte)1, (byte)0, COMMAND_IAC, COMMAND_SUBNEGOTIATION_END);
92 flush();
93 }
94
95 private void setResizeNotificationOn() throws IOException {
96 writeToTerminal(
97 COMMAND_IAC, COMMAND_DO, OPTION_NAWS);
98 flush();
99 }
100
101 /**
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
105 */
106 public NegotiationState getNegotiationState() {
107 return negotiationState;
108 }
109
110 /**
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
113 */
114 public void close() throws IOException {
115 socket.close();
116 }
117
118 /**
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.
121 */
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;
128
129 NegotiationState() {
130 this.clientEcho = true;
131 this.clientLineMode0 = false;
132 this.clientResizeNotification = false;
133 this.suppressGoAhead = true;
134 this.extendedAscii = true;
135 }
136
137 /**
138 * Is the telnet client echo mode turned on (client is echoing characters locally)
139 * @return {@code true} if client echo is enabled
140 */
141 public boolean isClientEcho() {
142 return clientEcho;
143 }
144
145 /**
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
148 */
149 public boolean isClientLineMode0() {
150 return clientLineMode0;
151 }
152
153 /**
154 * Is the telnet client resize notification turned on (client notifies server when the terminal window has
155 * changed size)
156 * @return {@code true} if client resize notification is enabled
157 */
158 public boolean isClientResizeNotification() {
159 return clientResizeNotification;
160 }
161
162
163 /**
164 * Is the telnet client suppress go-ahead turned on
165 * @return {@code true} if client suppress go-ahead is enabled
166 */
167 public boolean isSuppressGoAhead() {
168 return suppressGoAhead;
169 }
170
171 /**
172 * Is the telnet client extended ascii turned on
173 * @return {@code true} if client extended ascii is enabled
174 */
175 public boolean isExtendedAscii() {
176 return extendedAscii;
177 }
178
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));
181 }
182
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));
185 }
186
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));
190 }
191
192 private static List<String> toList(byte[] array) {
193 List<String> list = new ArrayList<String>(array.length);
194 for(byte b: array) {
195 list.add(String.format("%02X ", b));
196 }
197 return list;
198 }
199 }
200
201 private interface TelnetClientEventListener {
202 void onResize(int columns, int rows);
203 void requestReply(boolean will, byte option) throws IOException;
204 }
205
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;
213
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;
221 }
222
223 private void setEventListener(TelnetClientEventListener eventListener) {
224 this.eventListener = eventListener;
225 }
226
227 @Override
228 public int read() throws IOException {
229 throw new UnsupportedOperationException("TelnetClientIACFilterer doesn't support .read()");
230 }
231
232 @Override
233 public void close() throws IOException {
234 inputStream.close();
235 }
236
237 @Override
238 public int available() throws IOException {
239 int underlyingStreamAvailable = inputStream.available();
240 if(underlyingStreamAvailable == 0 && bytesInBuffer == 0) {
241 return 0;
242 }
243 else if(underlyingStreamAvailable == 0) {
244 return bytesInBuffer;
245 }
246 else if(bytesInBuffer == buffer.length) {
247 return bytesInBuffer;
248 }
249 fillBuffer();
250 return bytesInBuffer;
251 }
252
253 @Override
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) {
257 fillBuffer();
258 }
259 if(bytesInBuffer == 0) {
260 return -1;
261 }
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;
266 return bytesToCopy;
267 }
268
269 private void fillBuffer() throws IOException {
270 int readBytes = inputStream.read(workingBuffer, 0, Math.min(workingBuffer.length, buffer.length - bytesInBuffer));
271 if(readBytes == -1) {
272 return;
273 }
274 for(int i = 0; i < readBytes; i++) {
275 if(workingBuffer[i] == COMMAND_IAC) {
276 i++;
277 if(Arrays.asList(COMMAND_DO, COMMAND_DONT, COMMAND_WILL, COMMAND_WONT).contains(workingBuffer[i])) {
278 parseCommand(workingBuffer, i, readBytes);
279 ++i;
280 continue;
281 }
282 else if(workingBuffer[i] == COMMAND_SUBNEGOTIATION) { //0xFA = SB = Subnegotiation
283 i += parseSubNegotiation(workingBuffer, ++i, readBytes);
284 continue;
285 }
286 else if(workingBuffer[i] != COMMAND_IAC) { //Double IAC = 255
287 System.err.println("Unknown Telnet command: " + workingBuffer[i]);
288 }
289 }
290 buffer[bytesInBuffer++] = workingBuffer[i];
291 }
292 }
293
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");
298 }
299 byte command = buffer[position];
300 byte value = buffer[position + 1];
301 switch(command) {
302 case COMMAND_DO:
303 case COMMAND_DONT:
304 if(value == OPTION_SUPPRESS_GO_AHEAD) {
305 negotiationState.suppressGoAhead = (command == COMMAND_DO);
306 eventListener.requestReply(command == COMMAND_DO, value);
307 }
308 else if(value == OPTION_EXTEND_ASCII) {
309 negotiationState.extendedAscii = (command == COMMAND_DO);
310 eventListener.requestReply(command == COMMAND_DO, value);
311 }
312 else {
313 negotiationState.onUnsupportedRequestCommand(command == COMMAND_DO, value);
314 }
315 break;
316 case COMMAND_WILL:
317 case COMMAND_WONT:
318 if(value == OPTION_ECHO) {
319 negotiationState.clientEcho = (command == COMMAND_WILL);
320 }
321 else if(value == OPTION_LINEMODE) {
322 negotiationState.clientLineMode0 = (command == COMMAND_WILL);
323 }
324 else if(value == OPTION_NAWS) {
325 negotiationState.clientResizeNotification = (command == COMMAND_WILL);
326 }
327 else {
328 negotiationState.onUnsupportedStateCommand(command == COMMAND_WILL, value);
329 }
330 break;
331 default:
332 throw new UnsupportedOperationException("No command handler implemented for " + TelnetProtocol.CODE_TO_NAME.get(command));
333 }
334 }
335
336 private int parseSubNegotiation(byte[] buffer, int position, int max) {
337 int originalPosition = position;
338
339 //Read operation
340 byte operation = buffer[position++];
341
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);
348 }
349 else {
350 if(position + 1 == max) {
351 throw new IllegalStateException("State error, unexpected end of buffer when reading subnegotiation");
352 }
353 position++;
354 if(buffer[position] == COMMAND_IAC) {
355 outputBuffer.write(COMMAND_IAC); //Escaped IAC
356 }
357 else if(buffer[position] == COMMAND_SUBNEGOTIATION_END) {
358 parseSubNegotiation(operation, outputBuffer.toByteArray());
359 return ++position - originalPosition;
360 }
361 }
362 position++;
363 }
364 throw new IllegalStateException("State error, unexpected end of buffer when reading subnegotiation, no IAC SE");
365 }
366
367 private void parseSubNegotiation(byte option, byte[] additionalData) {
368 switch(option) {
369 case OPTION_NAWS:
370 eventListener.onResize(
371 convertTwoBytesToInt2(additionalData[1], additionalData[0]),
372 convertTwoBytesToInt2(additionalData[3], additionalData[2]));
373 break;
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
377 break;
378 default:
379 negotiationState.onUnsupportedSubnegotiation(option, additionalData);
380 break;
381 }
382 }
383 }
384
385 private static int convertTwoBytesToInt2(byte b1, byte b2) {
386 return ( (b2 & 0xFF) << 8) | (b1 & 0xFF);
387 }
388}