Commit | Line | Data |
---|---|---|
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 | */ | |
19 | package com.googlecode.lanterna.terminal.ansi; | |
20 | ||
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; | |
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 | */ | |
45 | public 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 | } |