Skip to content

Latest commit

 

History

History
178 lines (138 loc) · 8.51 KB

commands.md

File metadata and controls

178 lines (138 loc) · 8.51 KB

Commands infrastucture

FileSystemFactory usage

This interface is used to provide "file"-related services/commands - e.g., SCP and SFTP - although it can be used for remote command execution as well (see the section about commands and the Aware interfaces). The default implementation is a NativeFileSystemFactory that simply exposes the FileSystems.getDefault() result. However, for "sandboxed" implementations one can use the VirtualFileSystemFactory. This implementation provides a way for deciding what is the logged-in user's file system view and then use a RootedFileSystemProvider in order to provide a "sandboxed" file system where the logged-in user can access only the files under the specified root and no others.

SshServer sshd = SshServer.setUpDefaultServer();
sshd.setFileSystemFactory(new VirtualFileSystemFactory() {
    @Override
    public Path getUserHomeDir(SessionContext session) throws IOException {
        ...use whatever information ...
        return somePath;
    }
});

The usage of a FileSystemFactory is not limited though to the server only - the ScpClient implementation also uses it in order to retrieve the local path for upload/download-ing files/folders. This means that the client side can also be tailored to present different views for different clients. A special "empty" NoneFileSystemFactory is provided in case no files are expected to be accessed by the server.

ExecutorService-s

The framework requires from time to time spawning some threads in order to function correctly - e.g., commands, SFTP subsystem, port forwarding (among others) require such support. By default, the framework will allocate an ExecutorService for each specific purpose and then shut it down when the module has completed its work - e.g., session was closed. Note that SSHD uses the CloseableExecutorService interface instead of the usual ExecutorService in order to provide graceful shutdown. Users may provide their own CloseableExecutorService(s) instead of the internally auto-allocated ones - e.g., in order to control the max. spawned threads, stack size, track threads, etc... but they can leverage the SshThreadPoolExecutor implementation which should cover most use cases.

Users who want to provide their own ExecutorService and not use SshThreadPoolExecutor should wrap it as a NoCloseExecutor and take care of shutting it down when SSHD is done with (provided, of course, that the user's own code does not need it to remain active afterwards...).

/*
 * An example user-provided executor service for SFTP - there are other such locations.
 * By default, the SftpSubsystem implementation creates a single-threaded executor
 * for each session, uses it to spawn the SFTP command handler and shuts
 * it down when the command is destroyed
 */
SftpSubsystemFactory factory = new SftpSubsystemFactory.Builder()
    .withExecutorServiceProvider(() -> new NoCloseExecutor(mySuperDuperExecutorService))
    .build();
SshServer sshd = SshServer.setUpDefaultServer();
sshd.setSubsystemFactories(Collections.<NamedFactory<Command>>singletonList(factory));

If a single CloseableExecutorService is shared between several services, it needs to be wrapped with the ThreadUtils.noClose(executor) method.

CloseableExecutorService sharedService = ...obtain/create an instance...;

SftpSubsystemFactory factory = new SftpSubsystemFactory.Builder()
    .withExecutorServiceProvider(() -> ThreadUtils.noClose(sharedService))
    .build();

ChannelAgentForwarding forward = new ChannelAgentForwarding(ThreadUtils.noClose(sharedService));

Note: Do not share the instance returned by ThreadUtils.noClose between services as it interferes with the graceful closing mechanism. Use a new wrapper instance for each service.

Remote command execution

All command execution - be it shell or single command - boils down to a Command instance being created, initialized and then started. In this context, it is crucial to notice that the command's start() method implementation must spawn a new thread - even for the simplest or most trivial command. Any attempt to communicate via the established session will most likely fail since the packets processing thread may be blocked by this call. Note: one might get away with executing some command in the context of the thread that called the start() method, but it is extremely dangerous and should not be attempted.

The command execution code can communicate with the peer client via the input/output/error streams that are provided as part of the command initialization process. Once the command is done, it should call the ExitCallback#onExit method to indicate that it has finished. The framework will then take care of propagating the exit code, closing the session and (eventually) destroy()-ing the command. Note: the command may not assume that it is done until its destroy() method is called - i.e., it should not release or null-ify any of its internal state even if onExit() was called.

Upon calling the onExit method the code sends an SSH_MSG_CHANNEL_EOF message, and the provided result status code is sent as an exit-status message as described in RFC4254 - section 6.10. The provided message is simply logged at DEBUG level.

// A simple command implementation example
class MyCommand implements Command, Runnable {
    private InputStream in;
    private OutputStream out, err;
    private ExitCallback callback;

    public MyCommand() {
        super();
    }

    @Override
    public void setInputStream(InputStream in) {
        this.in = in;
    }

    @Override
    public void setOutputStream(OutputStream out) {
        this.out = out;
    }

    @Override
    public void setErrorStream(OutputStream err) {
        this.err = err;
    }

    @Override
    public void setExitCallback(ExitCallback callback) {
        this.callback = callback;
    }

    @Override
    public void start(Environment env) throws IOException {
        spawnHandlerThread(this);
    }

    @Override
    public void run() {
        while(true) {
            try {
                String cmd = readCommand(in);
                if ("exit".equals(cmd)) {
                    break;
                }

                handleCommand(cmd, out);
            } catch (Exception e) {
                writeError(err, e);
                callback.onExit(-1, e.getMessage());
                return;
        }

        callback.onExit(0);
    }

    @Override
    public void destroy() throws Exception {
        ...release any allocated resources...
    }
}

Aware interfaces

Once created, the Command instance is checked to see if it implements one of the Aware interfaces that enables injecting some dynamic data before the command is start()-ed.

  • SessionAware - Injects the Session instance through which the command request was received.

  • ChannelSessionAware - Injects the ChannelSession instance through which the command request was received.

  • FileSystemAware - Injects the result of consulting the FileSystemFactory as to the FileSystem associated with this command.

Data stream(s) sizing consideration

Some commands may send/receive large amounts of data over their STDIN/STDOUT/STDERR streams. Since (by default) the sending mechanism in SSHD is asynchronous it may cause Out of memory errors due to one side (client/server) generating SSH_MSG_CHANNEL_DATA or SSH_MSG_CHANNEL_EXTENDED_DATA at a much higher rate than the other side can consume. This leads to a build-up of a packets backlog that eventually consumes all available memory (as described in SSHD-754 and SSHD-768). As of version 1.7 one can register a ChannelStreamPacketWriterResolver at the client/server/session/channel level that can enable the user to replace the raw channel with some throttling mechanism that will be used for stream packets. Such an (experimental) example is the ThrottlingPacketWriter available in the sshd-contrib module. Note: if the ChannelStreamPacketWriterResolver returns a wrapper instance instead of a Channel then it will be closed automatically when the stream using it is closed.