001: /******************************************************************************
002: * $Source: /cvsroot/sshwebproxy/src/java/com/ericdaugherty/sshwebproxy/ShellChannel.java,v $
003: * $Revision: 1.4 $
004: * $Author: edaugherty $
005: * $Date: 2003/12/09 02:52:24 $
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.LogFactory;
042: import org.apache.commons.logging.Log;
043:
044: import java.util.ArrayList;
045: import java.io.*;
046:
047: import com.sshtools.j2ssh.session.SessionChannelClient;
048:
049: /**
050: * Provide an implementation of the SshChannel for interactive
051: * shell sessions.
052: * <p>
053: * While this is a concrete class that can be instantiated,
054: * the VT100ShellChannel should be used as the default ShellChannel
055: * because it provides a more robust shell implementation. This class
056: * should be extended by other terminal emulation classes, or instantiated
057: * directly for a very simple shell interaction.
058: *
059: * @author Eric Daugherty
060: */
061: public class ShellChannel extends SshChannel implements SshConstants {
062:
063: //***************************************************************
064: // Variables
065: //***************************************************************
066:
067: /** The number of columns to display as a screen */
068: protected int screenWidth = 80;
069:
070: /** The number of rows to display as a screen */
071: protected int screenHeight = 24;
072:
073: /** The total number of rows to store */
074: private int bufferMaxSize = 1000;
075:
076: /** The number of milliseconds to pause before reading */
077: private int readPause = 250;
078:
079: /** The size of the buffer to read from the server */
080: private int readBufferSize = 4048;
081:
082: /** The row that the cursor is currently on */
083: private int cursorRow = -1;
084:
085: /** The column that the cursor is currently on */
086: private int cursorColumn = -1;
087:
088: /** The Channel for the current Shell Connection */
089: protected SessionChannelClient sshChannel;
090:
091: /** The Input Reader for the SSH shell connection. */
092: private BufferedReader reader;
093:
094: /** The Output Writer for the SSH shell connection */
095: private PrintWriter writer;
096:
097: /** The entire stored buffer. */
098: private ArrayList buffer;
099:
100: /** Logger */
101: private static final Log log = LogFactory
102: .getLog(ShellChannel.class);
103:
104: //***************************************************************
105: // Constructor
106: //***************************************************************
107:
108: /**
109: * Opens a vt100 terminal session transfer session with the server.
110: *
111: * @param sshConnection the connection to use.
112: * @param sshChannel the SSH API channel.
113: * @throws SshConnectException thrown if there is any error opening
114: * the connection.
115: */
116: public ShellChannel(SshConnection sshConnection,
117: SessionChannelClient sshChannel) throws SshConnectException {
118: super (CHANNEL_TYPE_SHELL, sshConnection);
119:
120: this .sshChannel = sshChannel;
121:
122: // Initialize the channel for a shell session.
123: try {
124: if (!sshChannel.requestPseudoTerminal("vt100",
125: getScreenWidth(), getScreenHeight(), 0, 0, "")) {
126: log
127: .warn("ShellChannel constructor failed, unable to open PseudoTerminal for connection: "
128: + sshConnection.getConnectionInfo());
129: throw new SshConnectException(
130: "Unable to establish PseudoTerminal for new ShellChannel.");
131: } else if (!sshChannel.startShell()) {
132: log
133: .warn("ShellChannel constructor failed, unable to start shell on new channel for connection: "
134: + sshConnection.getConnectionInfo());
135: throw new SshConnectException(
136: "Unable to start Shell for new ShellChannel.");
137: }
138:
139: writer = new PrintWriter(new OutputStreamWriter(sshChannel
140: .getOutputStream()));
141: reader = new BufferedReader(new InputStreamReader(
142: sshChannel.getInputStream()));
143:
144: buffer = new ArrayList(bufferMaxSize);
145: } catch (IOException ioException) {
146: log
147: .warn(
148: "ShellChannel constructor failed, IOException occured while setting up channel for connection: "
149: + sshConnection.getConnectionInfo()
150: + ". IOException: " + ioException,
151: ioException);
152: throw new SshConnectException(
153: "Unable to establish Shell Connection. IOExeption occured: "
154: + ioException);
155: }
156: }
157:
158: //***************************************************************
159: // SshChannel Methods
160: //***************************************************************
161:
162: /**
163: * Closes the Reader and Writer after the Channel has been closed.
164: * This should only be called by the SshConnection
165: * class and never directly called from this class.
166: */
167: public void close() {
168: // Close Readers and Writers.
169: if (log.isInfoEnabled())
170: log.debug("Closing ShellChannel connected to: "
171: + sshConnection.getConnectionInfo());
172:
173: if (reader != null) {
174: try {
175: reader.close();
176: } catch (IOException ioException) {
177: log
178: .warn("Error closing BufferedReader for Shell Connection to: "
179: + sshConnection.getConnectionInfo()
180: + ". IOException: " + ioException);
181: }
182: reader = null;
183: }
184: if (writer != null) {
185: writer.close();
186: writer = null;
187: }
188:
189: // Close the channel if it is open.
190: if (sshChannel.isOpen()) {
191: try {
192: sshChannel.close();
193: } catch (IOException ioException) {
194: log
195: .warn("Error closing SessionChannelClient for Shell Connection to: "
196: + sshConnection.getConnectionInfo()
197: + ". IOException: " + ioException);
198: }
199: }
200: }
201:
202: /**
203: * Indicates whether this connection is still active.
204: *
205: * @return true if this connection is still active.
206: */
207: public boolean isConnected() {
208: return !sshChannel.isClosed();
209: }
210:
211: /**
212: * Returns the page that should be used to display this Channel.
213: *
214: * @return
215: */
216: public String getPage() {
217: return PAGE_SHELL_HOME + "?connection="
218: + sshConnection.getConnectionInfo() + "&channel="
219: + getChannelId();
220: }
221:
222: //***************************************************************
223: // Public Parameter Access
224: //***************************************************************
225:
226: /**
227: * The number of columns to display as a screen
228: */
229: public int getScreenWidth() {
230: return screenWidth;
231: }
232:
233: /**
234: * The number of rows to display as a screen
235: */
236: public int getScreenHeight() {
237: return screenHeight;
238: }
239:
240: /**
241: * The total number of rows to store in the buffer.
242: *
243: * @return number of rows to store.
244: */
245: public int getBufferMaxSize() {
246: return bufferMaxSize;
247: }
248:
249: /**
250: * The number of milliseconds to pause before reading
251: * data. This helps reduce the number of read requests
252: * after a write request.
253: *
254: * @return the number of milliseconds to pause.
255: */
256: public int getReadPause() {
257: return readPause;
258: }
259:
260: /**
261: * The maximum amount of data to read from the server
262: * for each read() call.
263: *
264: * @return the max read buffer size.
265: */
266: public int getReadBufferSize() {
267: return readBufferSize;
268: }
269:
270: /**
271: * The index of the row the cursor is on. The cursor location
272: * is assumed to be after that last character from the server.
273: *
274: * @return the row index of the cursor.
275: */
276: public int getCursorRow() {
277: return cursorRow;
278: }
279:
280: /**
281: * The index of the row the cursor is on. The cursor location
282: * is assumed to be after that last character from the server.
283: *
284: * @return the column index of the cursor.
285: */
286: public int getCursorColumn() {
287: return cursorColumn;
288: }
289:
290: //***************************************************************
291: // Public Data Manipulation
292: //***************************************************************
293:
294: /**
295: * Performs a read of the input data and fills the buffer.
296: * This should be called before getScreen or getBuffer.
297: */
298: public void read() {
299: if (log.isDebugEnabled())
300: log.debug("read called for ShellConnection to: "
301: + sshConnection.getConnectionInfo());
302:
303: // We want to read even if the channel has been closed, because
304: // the BufferedReader may have buffered some input, so do the
305: // check after this read. But if the reader is null, just
306: // ignore the call.
307: if (reader == null) {
308: log.warn("read called on null reader. Ignoring.");
309: return;
310: }
311:
312: // Read from the server
313: try {
314: // Initialize the input buffer.
315: char[] inputBuffer = new char[readBufferSize];
316: String input = null;
317:
318: // Sleep for the read pause. This allows the server
319: // to send us the 'full' data. If we don't sleep,
320: // the user may just have to do a refresh right away
321: // anyway.
322: try {
323: Thread.sleep(getReadPause());
324: } catch (InterruptedException e) {
325: log.warn("Read Pause interrupted in read().");
326: }
327:
328: // If there is data ready, go ahead and read it.
329: if (reader.ready()) {
330: // read the data and run it through the processor.
331: int count = reader.read(inputBuffer);
332: input = process(inputBuffer, count);
333: if (log.isDebugEnabled())
334: log.debug("Read " + count
335: + " characters from server.");
336:
337: fillBuffer(input);
338:
339: // Check to see if the channel was closed.
340: if (!isConnected()) {
341: if (!reader.ready()) {
342: if (log.isDebugEnabled())
343: log.debug("ShellChannel for connecton: "
344: + sshConnection.getConnectionInfo()
345: + " Closed, closing streams.");
346:
347: // Notify the sshConnection that this channel is closed.
348: sshConnection.closeChannel(this );
349: } else {
350: if (log.isDebugEnabled())
351: log
352: .debug("Connection Closed but there is more data to be read.");
353: }
354: }
355: } else {
356: log.debug("ShellChannel for connection: "
357: + sshConnection.getConnectionInfo()
358: + " has no data to read.");
359: }
360: } catch (IOException ioException) {
361: log.error("Error reading ShellChannel for connection: "
362: + sshConnection.getConnectionInfo()
363: + ". IOException while in read(): " + ioException,
364: ioException);
365: }
366: }
367:
368: /**
369: * Writes the data to the SSH server and sends a newline charecter
370: * "\n" if the sendNewLine boolean is true.
371: *
372: * @param data the data to write.
373: * @param sendNewLine true if a newline should be sent.
374: */
375: public void write(String data, boolean sendNewLine) {
376: // Don't write if the channel is closed.
377: if (!isConnected()) {
378: log
379: .info("Write call on closed ShellChannel for connection: "
380: + sshConnection.getConnectionInfo()
381: + ". Ignoring.");
382: return;
383: }
384:
385: // Verify the writer is not null.
386: if (writer == null) {
387: log
388: .info("Write call on closed ShellChannel Writer for connection: "
389: + sshConnection.getConnectionInfo()
390: + ". Ignoring.");
391: return;
392: }
393:
394: // Encode the data for output. Convert any control characters to
395: // the correct char value.
396: char[] output = encodeOutput(data);
397:
398: if (log.isDebugEnabled())
399: log.debug("Wrote " + output.length
400: + " characters to ShellChannel for connection: "
401: + sshConnection.getConnectionInfo());
402:
403: // Write the output, and send a new line if requested.
404: writer.print(output);
405: if (sendNewLine) {
406: writer.print("\n");
407: }
408: writer.flush();
409: }
410:
411: /**
412: * Adds the data that was read to the buffer.
413: *
414: * @param input the processed data read from the server
415: */
416: public void fillBuffer(String input) {
417: // Add data to the buffer.
418: String[] lines = input.split("\r\n");
419: int startIndex = 0;
420:
421: // Append the first line on the end of the last line.
422: if (buffer.size() > 0 && lines.length > 0) {
423: int lastIndex = buffer.size() - 1;
424: buffer.set(lastIndex, ((String) buffer.get(lastIndex))
425: + lines[0]);
426: startIndex = 1;
427: }
428:
429: // Add the rest of the new lines.
430: for (int index = startIndex; index < lines.length; index++) {
431: buffer.add(lines[index]);
432: }
433:
434: // Append a new empty line if the last line ended with a line feed.
435: if (input.lastIndexOf("\r\n") == input.length() - 2) {
436: buffer.add("");
437: }
438:
439: // Remove any extra rows from the begining of the buffer.
440: int currentBufferSize = buffer.size();
441: if (currentBufferSize > bufferMaxSize) {
442: int trimCount = currentBufferSize - bufferMaxSize;
443: if (log.isDebugEnabled())
444: log.debug("Removing " + trimCount
445: + " rows from the buffer.");
446: for (int index = 0; index < trimCount; index++) {
447: buffer.remove(0);
448: }
449: }
450: }
451:
452: /**
453: * Returns a String array of the currently visible
454: * rows. The number of rows returned will always match
455: * the Screen Size.
456: *
457: * @return Array of Strings that represent the current data on the screen.
458: */
459: public String[] getScreen() {
460: String[] screen = new String[screenHeight];
461:
462: int currentBufferSize = buffer.size();
463:
464: if (currentBufferSize <= screenHeight) {
465: int index;
466: // Fill the screen array with the buffer.
467: for (index = 0; index < currentBufferSize; index++) {
468: screen[index] = (String) buffer.get(index);
469: }
470: // Fill out any remaining rows.
471: for (; index < screenHeight; index++) {
472: screen[index] = "";
473: }
474:
475: cursorRow = currentBufferSize - 1;
476: cursorColumn = -1;
477: } else {
478: int bufferIndex = currentBufferSize - screenHeight;
479: int screenIndex = 0;
480: for (; bufferIndex < currentBufferSize; bufferIndex++) {
481: screen[screenIndex++] = (String) buffer
482: .get(bufferIndex);
483: }
484:
485: cursorRow = screenHeight - 1;
486: cursorColumn = -1;
487: }
488:
489: return screen;
490: }
491:
492: /**
493: * Returns a String array of the entire buffer.
494: * rows.
495: *
496: * @return Array of Strings that represent the entire buffer.
497: */
498: public String[] getBuffer() {
499: int currentBufferSize = buffer.size();
500: String[] bufferArray = new String[currentBufferSize];
501:
502: for (int index = 0; index < currentBufferSize; index++) {
503: bufferArray[index] = (String) buffer.get(index);
504: }
505:
506: return bufferArray;
507: }
508:
509: /**
510: * Process the incoming request into a string.
511: * @return
512: */
513: protected String process(char[] inputBuffer, int count) {
514: return String.valueOf(inputBuffer, 0, count);
515: }
516:
517: /**
518: * Parse the data to write to the server for control characters.
519: *
520: * @param input the data read from the client.
521: * @return a char array to write to the server.
522: */
523: private char[] encodeOutput(String input) {
524: char[] translateBuffer = new char[input.length()];
525: int originalCount = input.length();
526: int outputCount = 0;
527: boolean ctrlPressed = false;
528:
529: for (int index = 0; index < originalCount; index++) {
530: // Check if the last key was a control key.
531: if (ctrlPressed == true) {
532: ctrlPressed = false;
533: String shiftedKey = String.valueOf(input.charAt(index));
534: shiftedKey = shiftedKey.toUpperCase();
535: char newChar = (char) (shiftedKey.charAt(0) - 64);
536: translateBuffer[outputCount++] = newChar;
537: }
538: // Encode control characters.
539: else if (input.charAt(index) == '#') {
540: // Make sure we have a full sequence.
541: if (input.length() < (index + 3)) {
542: log
543: .error("Invalid input data. Failed encoding. There must be 2 characters after the # character.");
544: return new char[0];
545: }
546:
547: try {
548: String charNumber = input.substring(index + 1,
549: index + 3);
550: int charValue = Integer.parseInt(charNumber, 16);
551: if (charValue == -1) {
552: ctrlPressed = true;
553: }
554: if (log.isDebugEnabled())
555: log.debug("Encoded #" + charNumber
556: + " to decimal: " + charValue);
557: index = index + 2;
558: translateBuffer[outputCount++] = (char) charValue;
559: } catch (NumberFormatException numberFormatException) {
560: log
561: .error("Invalid input data. failed encoding. The control character did not contain a valid hex value.");
562: return new char[0];
563: }
564: } else {
565: translateBuffer[outputCount++] = input.charAt(index);
566: }
567: }
568:
569: char[] outputBuffer = new char[outputCount];
570: for (int index = 0; index < outputCount; index++) {
571: outputBuffer[index] = translateBuffer[index];
572: }
573:
574: return outputBuffer;
575: }
576: }
|