All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sshtools.server.SessionChannelNG Maven / Gradle / Ivy

There is a newer version: 3.1.2
Show newest version
/**
 * (c) 2002-2021 JADAPTIVE Limited. All Rights Reserved.
 *
 * This file is part of the Maverick Synergy Java SSH API.
 *
 * Maverick Synergy is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Maverick Synergy is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Maverick Synergy.  If not, see .
 */

package com.sshtools.server;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import com.sshtools.common.command.ExecutableCommand;
import com.sshtools.common.events.Event;
import com.sshtools.common.events.EventCodes;
import com.sshtools.common.events.EventServiceImplementation;
import com.sshtools.common.logger.Log;
import com.sshtools.common.nio.IdleStateListener;
import com.sshtools.common.nio.WriteOperationRequest;
import com.sshtools.common.permissions.PermissionDeniedException;
import com.sshtools.common.policy.FileSystemPolicy;
import com.sshtools.common.shell.ShellPolicy;
import com.sshtools.common.ssh.ChannelOpenException;
import com.sshtools.common.ssh.SessionChannelHelper;
import com.sshtools.common.ssh.SessionChannelServer;
import com.sshtools.common.ssh.SshConnection;
import com.sshtools.common.ssh.Subsystem;
import com.sshtools.common.ssh.UnsupportedChannelException;
import com.sshtools.common.util.ByteArrayReader;
import com.sshtools.common.util.Utils;
import com.sshtools.synergy.ssh.ChannelNG;
import com.sshtools.synergy.ssh.ChannelOutputStream;

/**
 * 

* This class provides an abstract session, it handles all the requests defined * in the SSH Connection protocol for a session channel and passes the request * to the concrete implementation through its abstract methods. *

* *

* When creating a session implementation your {@link #processStdinData(byte[])} * will receive data from the client and process as session input. To send * stdout data for your session you should use the * {@link #sendStdoutData(byte[])} method and any sdterr output should be send * using {@link #sendStderrData(byte[])} method. *

* *

* As the server uses an asynchronous framework expensive blocking operations * SHOULD NOT be performed within your session as this will cause a deadlock on * the server. *

* *

* The basic process of establishing a session is this *

    *
  • 1. The session is opened
  • *
    *
  • 2. The client possibly sends requests for a pseudo terminal or setting of * environment variables
  • *
    *
  • 3. The client requests to either start a shell or execute a command. Once * this has been requested the interactive session starts and data can be sent * or received.
  • *
    *
  • 4. Data is sent/received until the command has completed. Once completed * the session should close the channel and optionally sent the exit status of * the command using {@link #sendExitStatus(int)}.
  • *
    *
  • 5. At anytime throughout the open session the client may send a signal. * See the SSH Connection Protocol specification for more information on this * advanced topic.
  • *
*

* * */ public abstract class SessionChannelNG extends ChannelNG implements IdleStateListener, SessionChannelServer { public static final int SSH_EXTENDED_DATA_STDERR = 1; Subsystem subsystem; ExecutableCommand command; Map environment = new ConcurrentHashMap(8, 0.9f, 1); boolean hasTimedOut = false; boolean haltIncomingData = false; long lastActivity = System.currentTimeMillis(); boolean agentForwardingRequested; boolean rawMode = false; ChannelOutputStream stderrOutputStream = new ChannelOutputStream(this, SSH_EXTENDED_DATA_STDERR); public SessionChannelNG(SshConnection con) { this(con, false); } public SessionChannelNG(SshConnection con, boolean autoConsume) { super("session", con.getContext().getPolicy(ShellPolicy.class).getSessionMaxPacketSize(), 0, con.getContext().getPolicy(ShellPolicy.class).getSessionMaxWindowSize(), con.getContext().getPolicy(ShellPolicy.class).getSessionMinWindowSize(), null, autoConsume); } public void enableRawMode() { rawMode = true; } public void disableRawMode() { rawMode = false; } public Subsystem getSubsystem() { return subsystem; } final protected byte[] createChannel() throws java.io.IOException { registerExtendedDataType(SSH_EXTENDED_DATA_STDERR); return null; } public OutputStream getErrorStream() { return stderrOutputStream; } public boolean isAgentForwardingRequested() { return agentForwardingRequested; } /** * Implement this method to support agent forwarding. * * @return */ protected boolean requestAgentForwarding(String requestType) { return false; } /** * If the client requests a pseudo terminal for the session this method will * be invoked before the shell, exec or subsystem is started. * * @param term * @param cols * @param rows * @param width * @param height * @param modes * @return boolean */ protected abstract boolean allocatePseudoTerminal(String term, int cols, int rows, int width, int height, byte[] modes); /** * When the window (terminal) size changes on the client side, it MAY send * notification in which case this method will be invoked to notify the * session that a change has occurred. * * @param cols * @param rows * @param width * @param height */ protected abstract void changeWindowDimensions(int cols, int rows, int width, int height); /** * A signal can be delivered to the process by the client. If a signal is * received this method will be invoked so that the session may evaluate and * take the required action. * * @param signal */ protected abstract void processSignal(String signal); /** * If the client requests that an environment variable be set this method * will be invoked. * * @param name * @param value * @return true if the variable has been set, otherwise * false */ public abstract boolean setEnvironmentVariable(String name, String value); /** * Invoked when the user wants to start a shell. * * @return true if the shell has been started, otherwise * false */ protected abstract boolean startShell(); /** * Invoked when the user wants to execute a command * * @param cmd * @return true if the cmd has been executed, otherwise * false */ protected boolean executeCommand(String[] args) { boolean success = false; try { command = connection.getContext().getChannelFactory().executeCommand(args, environment); success = true; } catch (UnsupportedChannelException | PermissionDeniedException e) { if(Log.isDebugEnabled()) Log.debug("Failed to execute command" , e); success = false; } return success; } /** * Called once the channel has been opened. */ protected void onChannelOpen() { if (getContext().getPolicy(ShellPolicy.class).getSessionTimeout() > 0) getConnectionProtocol().getTransport().getSocketConnection().getIdleStates() .register(this); } public boolean idle() { if (getContext().getPolicy(ShellPolicy.class).getSessionTimeout() > 0) { long idleTimeSeconds = (System.currentTimeMillis() - lastActivity) / 1000; if (getContext().getPolicy(ShellPolicy.class).getSessionTimeout() < idleTimeSeconds) { if(Log.isDebugEnabled()) Log.debug("Session has timed out!"); hasTimedOut = true; close(); return true; } } else return true; return false; } /** * Process session requests and invoke the relevant abstract methods of this * class to handle the requests. If you overide this method make sure that * you call the super method. * * @param type * String * @param wantreply * boolean * @param requestdata * byte[] */ protected void onChannelRequest(String type, boolean wantreply, byte[] requestdata) { boolean success = false; resetIdleState(); ByteArrayReader bar = new ByteArrayReader( requestdata == null ? new byte[] {} : requestdata); try { if (type.equals("pty-req")) { String term = bar.readString(); int cols = (int) bar.readInt(); int rows = (int) bar.readInt(); int width = (int) bar.readInt(); int height = (int) bar.readInt(); byte[] modes = bar.readBinaryString(); success = allocatePseudoTerminal(term, cols, rows, width, height, modes); if(Log.isDebugEnabled()) Log.debug(term + " pseudo terminal requested"); if(Log.isDebugEnabled()) Log.debug("Terminal dimensions are " + String.valueOf(cols) + "x" + String.valueOf(rows)); } else if (type.equals("x11-req")) { boolean singleConnection = bar.readBoolean(); String protocol = bar.readString(); String cookie = bar.readString(); int screen = (int) bar.readInt(); ByteArrayOutputStream out = new ByteArrayOutputStream(); for (int i = 0; i < cookie.length(); i += 2) { out.write(Integer.parseInt(cookie.substring(i, i + 2), 16)); } success = getContext().getForwardingManager() .startX11Forwarding(singleConnection, protocol, out.toByteArray(), screen, getConnectionProtocol()); } else if (type.equals("env")) { // read 'requestdata' as a byte stream. String name = bar.readString(); String value = bar.readString(); environment.put(name, value); success = setEnvironmentVariable(name, value); if(Log.isDebugEnabled()) Log.debug(name + "=" + value + " environment variable set"); } else if (type.equals("shell")) { boolean shellSuccess = connection.getContext().getPolicy(ShellPolicy.class).checkPermission( getConnection(), ShellPolicy.SHELL); if (shellSuccess) { success = startShell(); } else success = shellSuccess; EventServiceImplementation.getInstance().fireEvent( new Event(this, EventCodes.EVENT_SHELL_SESSION_STARTED, success).addAttribute( EventCodes.ATTRIBUTE_CONNECTION, getConnection())); if(Log.isDebugEnabled()) Log.debug("Shell " + (success ? "started" : "failed")); } else if (type.equals("exec")) { String cmd = bar.readString(); success = connection.getContext().getPolicy(ShellPolicy.class).checkPermission( getConnection(), ShellPolicy.EXEC, cmd); if(success) { success = executeCommand(Utils.splitToArgsArray(cmd)); } EventServiceImplementation.getInstance().fireEvent( new Event(this, EventCodes.EVENT_SHELL_COMMAND, success).addAttribute( EventCodes.ATTRIBUTE_CONNECTION, getConnection()) .addAttribute(EventCodes.ATTRIBUTE_COMMAND, cmd)); if(Log.isDebugEnabled()) Log.debug("Command " + cmd + (success ? " started" : " failed")); } else if (type.equals("subsystem")) { String name = bar.readString(); success = connection.getContext().getPolicy(ShellPolicy.class).checkPermission( getConnection(), ShellPolicy.SUBSYSTEM, name); if (success) { try { subsystem = connection.getContext().getChannelFactory().createSubsystem(name, this); } catch (UnsupportedChannelException e) { success = false; if(Log.isDebugEnabled()) { Log.debug(name + " is an unsupported subsystem"); } } catch (PermissionDeniedException e) { success = false; if(Log.isDebugEnabled()) { Log.debug(name + " could not be opened. Permission denied."); } } } if("sftp".equals(name)) { localWindow.setMaximumWindowSpace(connection.getContext().getPolicy(FileSystemPolicy.class).getSftpMaxWindowSize()) ; localWindow.setMinimumWindowSpace(connection.getContext().getPolicy(FileSystemPolicy.class).getSftpMinWindowSize()); localWindow.setMaxiumPacketSize(connection.getContext().getPolicy(FileSystemPolicy.class).getSftpMaxPacketSize()); } sendWindowAdjust(); } else if (type.equals("window-change")) { int cols = (int) bar.readInt(); int rows = (int) bar.readInt(); int width = (int) bar.readInt(); int height = (int) bar.readInt(); changeWindowDimensions(cols, rows, width, height); } else if (type.equals("signal")) { String signal = bar.readString(); processSignal(signal); } else if(type.equals("auth-agent-req")) { if(Log.isDebugEnabled()) { Log.debug("Agent forwarding requested for auth-agent"); } success = agentForwardingRequested = requestAgentForwarding("auth-agent"); } else if(type.equals("[email protected]")) { if(Log.isDebugEnabled()) { Log.debug("Agent forwarding requested for [email protected]"); } success = agentForwardingRequested = requestAgentForwarding("[email protected]"); } } catch (IOException ex) { if(Log.isDebugEnabled()) Log.debug("An unexpected exception occurred", ex); } finally { bar.close(); } if (success && (type.equals("exec") || type.equals("shell"))) { // We can now send data so send a window adjust sendWindowAdjust(); if (command == null) { resetIdleState(); onSessionOpen(); } // Moved here because it seems some clients do not like the // WINDOW_ADJUST message after this has been sent. if (wantreply) { sendRequestResponse(success); } // Ensure command is started AFTER response is sent back. if (command != null) command.start(); } else { // Moved here because it seems some clients do not like the // WINDOW_ADJUST message after this has been sent. if (wantreply) { sendRequestResponse(success); } } // Close channel only after sending response to request if (!success && (type.equals("exec") || type.equals("shell") || type .equals("subsystem"))) { close(); } } // boolean checkForExecutableCommand(String cmd) { // // int idx = cmd.indexOf(' '); // String exec; // if (idx > -1) { // exec = cmd.substring(0, idx); // } else // exec = cmd; // // if (connection.getContext().containsCommand(exec)) { // try { // command = connection.getContext().getCommand(exec).newInstance(); // command.init(this); // return command.createProcess(cmd, environment); // } catch (IllegalAccessException ex) { // if(Log.isDebugEnabled()) // Log.debug("Failed to create an ExecutableCommand", ex); // } catch (InstantiationException ex) { // if(Log.isDebugEnabled()) // Log.debug("Failed to instantiate an ExecutableCommand", ex); // } // } // // Check the command here and run it // return false; // } /** * Called when the channel is confirmed as open */ protected void onChannelOpenConfirmation() { } /** * The remote side has reported EOF so no more data will be received. This * will force the channel to close. If this behaviour is not required you * can override this method */ protected void onRemoteEOF() { close(); } /** * Free the session and its resources. If you override this method make sure * that you call the super method to ensure that the resources of the * abstract class are freed. */ protected void onChannelFree() { if (subsystem != null) { subsystem.free(); } subsystem = null; command = null; } /** * Called when the channel is closing. If you override this method make sure * that you call the super method. */ protected void onChannelClosing() { if (getContext().getPolicy(ShellPolicy.class).getSessionTimeout() > 0 && !hasTimedOut) getConnectionProtocol().getTransport().getSocketConnection().getIdleStates() .remove(this); if (command != null) { if (command.getExitCode() != ExecutableCommand.STILL_ACTIVE) { SessionChannelHelper.sendExitStatus(this, command.getExitCode()); } else { command.kill(); } } } private void resetIdleState() { lastActivity = System.currentTimeMillis(); if (getContext().getPolicy(ShellPolicy.class).getSessionTimeout() > 0) getConnectionProtocol().getTransport().getSocketConnection().getIdleStates() .reset(this); } /** * Called when data arrives on the channel. * * @param data * byte[] */ protected final void onChannelData(ByteBuffer data) { resetIdleState(); if (subsystem != null) { try { subsystem.processMessage(data); } catch (IOException ex) { if(Log.isDebugEnabled()) Log.debug( "The channel failed to process a subsystem message", ex); close(); } } else { if(rawMode) { super.onChannelData(data); } else { onSessionData(data); } } } protected void onSessionData(ByteBuffer data) { synchronized (localWindow) { cache.put(data); } } /** * Called when extended data arrives on the channel - for a session channel * this would not normally be called. * * @param data * byte[] * @param type * int */ protected void onExtendedData(ByteBuffer data, int type) { resetIdleState(); // No such thing as stderr coming from client to server? } /** * Sends stdout data to the remote client. * * @param data * @param off * @param len */ public void sendStdoutData(byte[] data, int off, int len) throws IOException { resetIdleState(); sendData(data, off, len); } /** * Sends stdout data to the remote client * * @param data */ public void sendStdoutData(byte[] data) throws IOException { resetIdleState(); sendData(data, 0, data.length); } /** * Sends stderr data to the remote client. * * @param data * @param off * @param len */ public void sendStderrData(byte[] data, int off, int len) throws IOException { resetIdleState(); sendExtendedData(data, off, len, SSH_EXTENDED_DATA_STDERR); } /** * Send stderr data to the remote client. * * @param data */ public void sendStderrData(byte[] data) throws IOException { sendStderrData(data, 0, data.length); } final protected byte[] openChannel(byte[] data) throws WriteOperationRequest, ChannelOpenException { registerExtendedDataType(SSH_EXTENDED_DATA_STDERR); return null; } public boolean isIncomingDataHalted() { return haltIncomingData; } @Override public int getMaximumWindowSpace() { return localWindow.getMaximumWindowSpace(); } @Override public int getMinimumWindowSpace() { return localWindow.getMinimumWindowSpace(); } @Override public void onSessionOpen() { } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy