1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
|
# 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()](https://docs.oracle.com/javase/8/docs/api/java/nio/file/FileSystems.html#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.
```java
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](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html)
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...).
```java
/*
* 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.
```java
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](https://tools.ietf.org/html/rfc4254#section-5.3) message,
and the provided result status code is sent as an `exit-status` message as described in [RFC4254 - section 6.10](https://tools.ietf.org/html/rfc4254#section-6.10).
The provided message is simply logged at DEBUG level.
```java
// 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](https://docs.oracle.com/javase/8/docs/api/java/nio/file/FileSystem.html)
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](https://issues.apache.org/jira/browse/SSHD-754) and [SSHD-768](https://issues.apache.org/jira/browse/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.
|