001: /******************************************************************************
002: * $Source: /cvsroot/sshwebproxy/src/java/com/ericdaugherty/sshwebproxy/VT100ShellChannel.java,v $
003: * $Revision: 1.2 $
004: * $Author: edaugherty $
005: * $Date: 2003/12/07 23:07:44 $
006: ******************************************************************************
007: * Copyright (c) 2003, Eric Daugherty (http://www.ericdaugherty.com)
008: * All rights reserved.
009: *
010: * Redistribution and use in source and binary forms, with or without
011: * modification, are permitted provided that the following conditions are met:
012: *
013: * * Redistributions of source code must retain the above copyright notice,
014: * this list of conditions and the following disclaimer.
015: * * Redistributions in binary form must reproduce the above copyright
016: * notice, this list of conditions and the following disclaimer in the
017: * documentation and/or other materials provided with the distribution.
018: * * Neither the name of the Eric Daugherty nor the names of its
019: * contributors may be used to endorse or promote products derived
020: * from this software without specific prior written permission.
021: *
022: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
023: * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
024: * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
025: * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
026: * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
027: * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
028: * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
029: * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
030: * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
031: * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
032: * THE POSSIBILITY OF SUCH DAMAGE.
033: * *****************************************************************************
034: * For current versions and more information, please visit:
035: * http://www.ericdaugherty.com/dev/sshwebproxy
036: *
037: * or contact the author at:
038: * web@ericdaugherty.com
039: *****************************************************************************/package com.ericdaugherty.sshwebproxy;
040:
041: import org.apache.commons.logging.Log;
042: import org.apache.commons.logging.LogFactory;
043: import com.sshtools.j2ssh.session.SessionChannelClient;
044:
045: /**
046: * Extends the default ShellChannel to provide VT100 Terminal
047: * emulation.
048: *
049: * @author Eric Daugherty
050: */
051: public class VT100ShellChannel extends ShellChannel {
052: //***************************************************************
053: // Constants
054: //***************************************************************
055:
056: // Control Characters (all values are decimal)
057:
058: /** Backspace */
059: private static final int CTRL_BS = 8;
060:
061: /** Newline */
062: private static final int CTRL_NL = 10;
063:
064: /** Carriage Return */
065: private static final int CTRL_CR = 13;
066:
067: /** Cancel */
068: private static final int CTRL_CAN = 24;
069:
070: /** Substitute */
071: private static final int CTRL_SUB = 26;
072:
073: /** Escape */
074: private static final int CTRL_ESC = 27;
075:
076: // Escape Sequence Codes
077:
078: /** Start a control sequence */
079: private static final int ESC_CTRL = '[';
080:
081: /** Enter Application Keypad Mode */
082: private static final int ESC_ALT_KEYPAD_APPLICATION = '=';
083:
084: /** Enter Numeric Keypad Mode */
085: private static final int ESC_ALT_KEYPAD_NUMERIC = '>';
086:
087: // Control Sequence Terminators
088:
089: /** CUU -- Cursor Up -- ESC [ Pn A */
090: private static final int TERM_CUU = 'A';
091:
092: /** CUD -- Cursor Down -- ESC [ Pn B */
093: private static final int TERM_CUD = 'B';
094:
095: /** CUF -- Cursor Forward -- ESC [ Pn C */
096: private static final int TERM_CUF = 'C';
097:
098: /** CUB -- Cursor Backward -- ESC [ Pn D */
099: private static final int TERM_CUB = 'D';
100:
101: /** CUP -- Cursor Position -- ESC [ Pn ; Pn H */
102: private static final int TERM_CUP = 'H';
103:
104: /** Reverse Line Feed */
105: private static final int TERM_RLF = 'I';
106:
107: /** Erase to End of Screen */
108: private static final int TERM_EES = 'J';
109:
110: /** Erase to End of Line */
111: private static final int TERM_EEL = 'K';
112:
113: /** CUP -- Cursor Position -- ESC [ Pn ; Pn f */
114: private static final int TERM_CUP_2 = 'f';
115:
116: /** Set the Mode */
117: private static final int TERM_MODE = 'h';
118:
119: /** Reset the Mode */
120: private static final int TERM_MODE_RESET = 'l';
121:
122: /** SGR -- Select Graphic Rendition */
123: private static final int TERM_SGR = 'm';
124:
125: // Cursor Movement Constants
126:
127: /** Move cursor up one row */
128: private static final int CURSOR_UP = 0;
129:
130: /** Move cursor down one row */
131: private static final int CURSOR_DOWN = 1;
132:
133: /** Move cursor forward (right) one column */
134: private static final int CURSOR_FORWARD = 2;
135:
136: /** Move cursor back (left) one column */
137: private static final int CURSOR_BACK = 3;
138:
139: //***************************************************************
140: // Variables
141: //***************************************************************
142:
143: /** True if we are in direct cursor manipulation mode */
144: private boolean cursorMode = false;
145:
146: /** Current (row) location of the cursor */
147: private int cursorRow = 0;
148:
149: /** Current (column) location of the cursor */
150: private int cursorColumn = 0;
151:
152: /** The screen buffer used for cursor mode */
153: private char[][] screen;
154:
155: /** True if the last character was ESC */
156: private boolean inEscapeSequence = false;
157:
158: /** True if we are currently in the middle of a control sequence. */
159: private boolean inControlSequence = false;
160:
161: /** The current control sequence */
162: private char[] controlSequence = new char[100];
163:
164: /** The current size of the control sequence */
165: private int controlSequenceSize = 0;
166:
167: /** Logger */
168: private static Log log = LogFactory.getLog(VT100ShellChannel.class);
169:
170: //***************************************************************
171: // Constructor
172: //***************************************************************
173:
174: /**
175: * Initializes a new VT100ShellChannel. Uses ShellChannel constructor
176: * to setup the channel.
177: *
178: * @param sshConnection the connection to use.
179: * @param sshChannel the SSH API channel.
180: * @throws SshConnectException thrown if there is any error opening
181: * the connection.
182: */
183: public VT100ShellChannel(SshConnection sshConnection,
184: SessionChannelClient sshChannel) throws SshConnectException {
185: super (sshConnection, sshChannel);
186:
187: // Initialize the local screen buffer.
188: screen = new char[getScreenHeight()][getScreenWidth()];
189: clearScreen();
190: }
191:
192: //***************************************************************
193: // Static Public Methods
194: //***************************************************************
195:
196: /**
197: * Determines if the specified character is a control character.
198: * A control character is any character less then 32 or greater than
199: * 126 (decimal).
200: *
201: * @param character the character to test
202: * @return true if it is a control character.
203: */
204: public static boolean isControlChar(char character) {
205: return (character < 32 || character > 126);
206: }
207:
208: //***************************************************************
209: // Parameter Access Methods
210: //***************************************************************
211:
212: /**
213: * The index of the row the cursor is on, if we are
214: * in cursor mode. Otherwise, return the value of ShellChannel.getCursorRow().
215: *
216: * @return the row index of the cursor.
217: */
218: public int getCursorRow() {
219: if (cursorMode) {
220: return cursorRow;
221: } else {
222: return super .getCursorRow();
223: }
224: }
225:
226: /**
227: * The index of the column the cursor is on, if we are
228: * in cursor mode. Otherwise, return the value of ShellChannel.getCursorColumn().
229: *
230: * @return the column index of the cursor.
231: */
232: public int getCursorColumn() {
233: if (cursorMode) {
234: return cursorColumn;
235: } else {
236: return super .getCursorColumn();
237: }
238: }
239:
240: //***************************************************************
241: // Public Methods
242: //***************************************************************
243:
244: /**
245: * Parses the data read from the server for control characters.
246: *
247: * @param inputBuffer the data read from the server.
248: * @param count the number of bytes in the inputBuffer that are valid.
249: * @return a string containing only valid characters.
250: */
251: public String process(char[] inputBuffer, int count) {
252: StringBuffer output = new StringBuffer();
253: char currentCharacter;
254:
255: // Process each character
256: for (int index = 0; index < count; index++) {
257: currentCharacter = inputBuffer[index];
258:
259: // Last character was escape.
260: if (inEscapeSequence) {
261: if (currentCharacter == ESC_CTRL) {
262: inEscapeSequence = false;
263: inControlSequence = true;
264: controlSequenceSize = 0;
265: } else if (currentCharacter == CTRL_ESC) {
266: log
267: .debug("Received back-to-back escape characters!");
268: }
269: // Check to see if the escape sequence is complete.
270: else if (escapeSequenceComplete(currentCharacter)) {
271: inEscapeSequence = false;
272: }
273: // We received an escape code we can't handle. Ignore it.
274: else {
275: inEscapeSequence = false;
276: log.debug("Unknown Escape Code received: "
277: + (int) currentCharacter);
278: }
279: }
280: // Already in a control sequence.
281: else if (inControlSequence) {
282: // Abort the control sequence
283: if (currentCharacter == CTRL_CAN
284: || currentCharacter == CTRL_SUB) {
285: inControlSequence = false;
286: controlSequenceSize = 0;
287: }
288: // Abort current sequence and start a new one
289: else if (currentCharacter == CTRL_ESC) {
290: inEscapeSequence = true;
291: inControlSequence = false;
292: controlSequenceSize = 0;
293: }
294: // The sequence is complete, process it.
295: else if (sequenceComplete(currentCharacter)) {
296: inControlSequence = false;
297: controlSequenceSize = 0;
298: }
299: // Add the current character to the sequence.
300: else {
301: controlSequence[controlSequenceSize++] = currentCharacter;
302: }
303: }
304: // Not in a control sequence.
305: else {
306: // Escape character starts an escape sequence.
307: if (currentCharacter == CTRL_ESC) {
308: inEscapeSequence = true;
309: }
310: // Check for control characters. All processing is done
311: // in the method call.
312: else if (controlCharacter(currentCharacter)) {
313: // Handling is done in controlCharacter method
314: }
315: // Otherwise, it is a valid printable character.
316: else {
317: // If we are in cursorMode, add it to our local copy.
318: if (cursorMode) {
319: screen[cursorRow][cursorColumn] = currentCharacter;
320: cursorColumn++;
321:
322: // Handle 'off the screen'.
323: if (cursorColumn >= screenWidth) {
324: cursorColumn = 0;
325: cursorRow++;
326: }
327: }
328: // Only return it if we are not keeping out own copy.
329: else {
330: output.append(currentCharacter);
331: }
332: }
333: }
334: }
335: return output.toString();
336: }
337:
338: /**
339: * Returns a String array of the currently visible
340: * rows. The number of rows returned will always match
341: * the Screen Size.
342: *
343: * @return Array of Strings that represent the current data on the screen.
344: */
345: public String[] getScreen() {
346: // If we are not in cursorMode, just display the default screen.
347: if (!cursorMode) {
348: return super .getScreen();
349: }
350: // Otherwise, display our screen version.
351: else {
352: String[] output = new String[screenHeight];
353: for (int index = 0; index < screenHeight; index++) {
354: output[index] = String.valueOf(screen[index]);
355: }
356:
357: return output;
358: }
359: }
360:
361: //***************************************************************
362: // Private Helper Methods
363: //***************************************************************
364:
365: /**
366: * Check to see if the current character is a control character.
367: * If so, handle it and return true. Otherwise, return false.
368: *
369: * @param currentCharacter the character to handle.
370: * @return true if it is a control character.
371: */
372: private boolean controlCharacter(char currentCharacter) {
373: boolean returnValue = false;
374:
375: // Process the character according to mode.
376: if (cursorMode) {
377: returnValue = true;
378: if (currentCharacter == CTRL_BS) {
379: moveCursor(CURSOR_BACK, 1);
380: } else if (currentCharacter == CTRL_NL) {
381: moveCursor(CURSOR_DOWN, 1);
382: } else if (currentCharacter == CTRL_CR) {
383: cursorColumn = 0;
384: } else if (isControlChar(currentCharacter)) {
385: if (log.isDebugEnabled())
386: log.debug("Ignoring unknown control character: "
387: + (int) currentCharacter
388: + " in cursorMode.");
389: } else {
390: returnValue = false;
391: }
392: } else {
393: // Treat CR and NL as normal characters in default mode.
394: if (isControlChar(currentCharacter)
395: && currentCharacter != CTRL_NL
396: && currentCharacter != CTRL_CR) {
397: if (log.isDebugEnabled())
398: log.debug("Ignoring unknown control character: "
399: + (int) currentCharacter
400: + " in normal mode.");
401: returnValue = true;
402: }
403: }
404: return returnValue;
405: }
406:
407: /**
408: * Checks to see if this character ends a control sequence.
409: * If so, execute the sequence if it is recognized.
410: *
411: * @param currentCharacter the character to check.
412: * @return true if the sequence is complete.
413: */
414: private boolean sequenceComplete(char currentCharacter) {
415: boolean returnValue = true;
416:
417: if (currentCharacter == TERM_CUU) {
418: moveCursor(CURSOR_UP, getCursorArgument());
419: } else if (currentCharacter == TERM_CUD) {
420: moveCursor(CURSOR_DOWN, getCursorArgument());
421: } else if (currentCharacter == TERM_CUF) {
422: moveCursor(CURSOR_FORWARD, getCursorArgument());
423: } else if (currentCharacter == TERM_CUB) {
424: moveCursor(CURSOR_BACK, getCursorArgument());
425: } else if (currentCharacter == TERM_CUP
426: || currentCharacter == TERM_CUP_2) {
427: int[] cursorPosition = getCursorArguments();
428: cursorRow = cursorPosition[0];
429: cursorColumn = cursorPosition[1];
430: log.debug("Moved cursor to row:column" + cursorRow + ":"
431: + cursorColumn);
432: } else if (currentCharacter == TERM_RLF) {
433: log.debug("Received Reverse Line Feed Sequence: "
434: + String.valueOf(controlSequence, 0,
435: controlSequenceSize));
436: } else if (currentCharacter == TERM_EES) {
437: // If we are 'at home', just wack the entire screen.
438: if (cursorRow == 0 && cursorColumn == 0) {
439: clearScreen();
440: log.debug("Cleared entire screen.");
441: }
442: // Othwerwise, wack the rest of this row and all of the other rows.
443: else {
444: boolean first = true;
445: for (int rowIndex = cursorRow; rowIndex < screenHeight; rowIndex++) {
446: if (first) {
447: first = false;
448: for (int columnIndex = cursorColumn; columnIndex < screenWidth; columnIndex++) {
449: screen[cursorRow][columnIndex] = ' ';
450: }
451: } else {
452: clearLine(rowIndex);
453: }
454: }
455: if (log.isDebugEnabled())
456: log.debug("Cleared screen from row:column"
457: + cursorRow + ":" + cursorColumn);
458: }
459: } else if (currentCharacter == TERM_EEL) {
460: clearLine(cursorRow, cursorColumn);
461: if (log.isDebugEnabled())
462: log.debug("Cleared row from row:column" + cursorRow
463: + ":" + cursorColumn);
464: } else if (currentCharacter == TERM_MODE) {
465: String mode = String.valueOf(controlSequence, 0,
466: controlSequenceSize);
467: if ("?1".equals(mode)) {
468: cursorMode = true;
469: log.debug("Entering cursor mode.");
470: } else {
471: log.warn("Unknown mode requested: " + mode);
472: }
473: } else if (currentCharacter == TERM_MODE_RESET) {
474: String mode = String.valueOf(controlSequence, 0,
475: controlSequenceSize);
476: if ("?1".equals(mode)) {
477: cursorMode = false;
478: log.debug("Exiting cursor mode.");
479: } else {
480: log.warn("Unknown mode requested: " + mode);
481: }
482: } else if (currentCharacter == TERM_SGR) {
483: log.warn("SGR Requested but ignored: "
484: + String.valueOf(controlSequence, 0,
485: controlSequenceSize));
486: } else {
487: returnValue = false;
488: }
489:
490: return returnValue;
491: }
492:
493: /**
494: * Checks for valid escape sequences.
495: *
496: * @param currentCharacter the character to test.
497: * @return true if we recognized it.
498: */
499: private boolean escapeSequenceComplete(char currentCharacter) {
500: if (currentCharacter == ESC_ALT_KEYPAD_APPLICATION) {
501: log.debug("Application Keypad Mode Enabled.");
502: return true;
503: }
504: if (currentCharacter == ESC_ALT_KEYPAD_NUMERIC) {
505: log.debug("Numeric Keypad Mode Enabled.");
506: return true;
507: }
508:
509: return false;
510: }
511:
512: /**
513: * Moves the the cursor a certain number of row or columns.
514: *
515: * @param mode The way to move the cursor.
516: * @param amount the length to move the cursor.
517: */
518: private void moveCursor(int mode, int amount) {
519: switch (mode) {
520: case CURSOR_UP:
521: cursorRow = cursorRow - amount;
522: validateRow();
523: if (log.isDebugEnabled())
524: log.debug("Moved cursor up " + amount + " rows.");
525: break;
526: case CURSOR_DOWN:
527: cursorRow = cursorRow + amount;
528: validateRow();
529: if (log.isDebugEnabled())
530: log.debug("Moved cursor down " + amount + " rows.");
531: break;
532: case CURSOR_FORWARD:
533: cursorColumn = cursorColumn + amount;
534: validateColumn();
535: if (log.isDebugEnabled())
536: log.debug("Moved cursor forward " + amount
537: + " columns.");
538: break;
539: case CURSOR_BACK:
540: cursorColumn = cursorColumn - amount;
541: validateColumn();
542: if (log.isDebugEnabled())
543: log.debug("Moved cursor back " + amount + " columns.");
544: break;
545: default:
546: log.error("Invalid mode argument in move cursor! " + mode);
547: break;
548: }
549: }
550:
551: /**
552: * Parses the command sequence for the number of
553: * rows or columns to move the character.
554: *
555: * @return parsed number of rows.
556: */
557: private int getCursorArgument() {
558: String argumentString = String.valueOf(controlSequence, 0,
559: controlSequenceSize);
560: if (argumentString == null || argumentString.length() == 0) {
561: log.debug("Empty cursor argument, defaulting to 1.");
562: return 1;
563: } else {
564: int argument = Integer.parseInt(argumentString);
565: if (log.isDebugEnabled())
566: log.debug("Cursor argument parsed as: " + argument);
567: return argument;
568: }
569: }
570:
571: /**
572: * Parses the command sequence for the number row and column
573: * position to move the cursor to.
574: *
575: * @return Array of size 2. 0 - row index, 1 - column index.
576: */
577: private int[] getCursorArguments() {
578: int[] position = new int[2];
579:
580: String argumentString = String.valueOf(controlSequence, 0,
581: controlSequenceSize);
582: int index = argumentString.indexOf(';');
583:
584: if (argumentString == null || argumentString.length() == 0
585: || index == -1) {
586: log
587: .debug("Empty cursor argument, defaulting to 1,1 (0,0).");
588: position[0] = 0;
589: position[1] = 0;
590: } else {
591: String[] arguments = argumentString.split(";");
592: String rowString = arguments[0];
593: String columnString = (arguments.length == 2) ? arguments[1]
594: : "";
595:
596: if (rowString == null || rowString.length() == 0) {
597: position[0] = 0;
598: } else {
599: position[0] = Integer.parseInt(rowString) - 1;
600: }
601:
602: if (columnString == null || columnString.length() == 0) {
603: position[1] = 0;
604: } else {
605: position[1] = Integer.parseInt(columnString) - 1;
606: }
607: if (log.isDebugEnabled())
608: log.debug("Cursor argument parsed as: " + position[0]
609: + ":" + position[1]);
610: }
611:
612: return position;
613: }
614:
615: /**
616: * Validates that the cursor is currently on the screen and moves
617: * it back to the first or last row if it is not.
618: */
619: private void validateRow() {
620: if (cursorRow >= screenHeight) {
621: log.warn("Cursor moved off screen!");
622: cursorRow = screenHeight - 1;
623: } else if (cursorRow < 0) {
624: log.warn("Cursor moved off screen!");
625: cursorRow = 0;
626: }
627: }
628:
629: /**
630: * Validates that the cursor is currently on the screen and moves
631: * it back to the first or last column if it is not.
632: */
633: private void validateColumn() {
634: if (cursorColumn >= screenWidth) {
635: log.warn("Cursor moved off screen!");
636: cursorColumn = screenWidth - 1;
637: } else if (cursorColumn < 0) {
638: log.warn("Cursor moved off screen!");
639: cursorColumn = 0;
640: }
641: }
642:
643: /** Clear the entire screen */
644: private void clearScreen() {
645: for (int index = 0; index < screenHeight; index++) {
646: clearLine(index);
647: }
648: }
649:
650: /**
651: * Clears an individual line.
652: *
653: * @param line the line to clearn.
654: */
655: private void clearLine(int line) {
656: clearLine(line, 0);
657: }
658:
659: /**
660: * Clears a line (row) starting with the specified index.
661: * @param line
662: * @param startIndex
663: */
664: private void clearLine(int line, int startIndex) {
665: for (int index = startIndex; index < screenWidth; index++) {
666: screen[line][index] = ' ';
667: }
668: }
669: }
|