This is an automated email from the ASF dual-hosted git repository. lgoldstein pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/mina-sshd.git
The following commit(s) were added to refs/heads/master by this push: new 43ab52f [SSHD-1057] Added capability to select a ShellFactory based on the current session + use it for WinSCP 43ab52f is described below commit 43ab52f7047b4a58f34b8a606d2fd5ef75688fcf Author: Lyor Goldstein <lgoldst...@apache.org> AuthorDate: Fri Aug 21 11:22:34 2020 +0300 [SSHD-1057] Added capability to select a ShellFactory based on the current session + use it for WinSCP --- CHANGES.md | 1 + docs/server-setup.md | 5 ++ .../sshd/cli/server/SshServerCliSupport.java | 35 +++++++-- .../org/apache/sshd/cli/server/SshServerMain.java | 9 +-- .../sshd/server/shell/AggregateShellFactory.java | 85 ++++++++++++++++++++++ .../sshd/server/shell/InvertedShellWrapper.java | 1 - .../sshd/server/shell/ShellFactorySelector.java | 66 +++++++++++++++++ .../apache/sshd/scp/server/ScpCommandFactory.java | 57 +++++++++++++-- 8 files changed, 238 insertions(+), 21 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 1a8973e..095744a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -43,4 +43,5 @@ or `-key-file` command line option. * [SSHD-1048](https://issues.apache.org/jira/browse/SSHD-1048) Wrap instead of rethrow IOException in Future. * [SSHD-1050](https://issues.apache.org/jira/browse/SSHD-1050) Fixed race condition in AuthFuture if exception caught before authentication started. * [SSHD-1056](https://issues.apache.org/jira/browse/SSHD-1005) Added support for SCP remote-to-remote directory transfer - including '-3' option of SCP command CLI. +* [SSHD-1058](https://issues.apache.org/jira/browse/SSHD-1057) Added capability to select a ShellFactory based on the current session + use it for "WinSCP" * [SSHD-1058](https://issues.apache.org/jira/browse/SSHD-1058) Improve exception logging strategy. diff --git a/docs/server-setup.md b/docs/server-setup.md index 3d46fbe..16e374c 100644 --- a/docs/server-setup.md +++ b/docs/server-setup.md @@ -54,6 +54,11 @@ so it's mostly useful to launch the OS native shell. E.g., There is an out-of-the-box `InteractiveProcessShellFactory` that detects the O/S and spawns the relevant shell. Note that the `ShellFactory` is not required. If none is configured, any request for an interactive shell will be denied to clients. +Furthermore, one can select a specific factory based on the current session by using an `AggregateShellFactory` that +wraps a group of `ShellFactorySelector` - each one tailored for a specific set of criteria. The simplest use-case is +one the detects the client and provides a specially tailored shell for it - e.g., +[the way we do for "WinSCP"](https://issues.apache.org/jira/browse/SSHD-1009) based on the peer client version string. + * `CommandFactory` - The `CommandFactory` provides the ability to run a **single** direct command at a time instead of an interactive session (it also uses a **different** channel type than shells). It can be used **in addition** to the `ShellFactory`. diff --git a/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerCliSupport.java b/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerCliSupport.java index d731231..6b3340b 100644 --- a/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerCliSupport.java +++ b/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerCliSupport.java @@ -62,6 +62,7 @@ import org.apache.sshd.server.forward.ForwardingFilter; import org.apache.sshd.server.keyprovider.AbstractGeneratorHostKeyProvider; import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; import org.apache.sshd.server.shell.InteractiveProcessShellFactory; +import org.apache.sshd.server.shell.ProcessShellCommandFactory; import org.apache.sshd.server.shell.ShellFactory; import org.apache.sshd.server.subsystem.SubsystemFactory; import org.apache.sshd.sftp.common.SftpConstants; @@ -274,20 +275,30 @@ public abstract class SshServerCliSupport extends CliSupport { return null; } + // Only SCP if (ScpCommandFactory.SCP_FACTORY_NAME.equalsIgnoreCase(factory)) { - ScpCommandFactory shell = new ScpCommandFactory(); - if (isEnabledVerbosityLogging(level)) { - shell.addEventListener(new ScpCommandTransferEventListener(stdout, stderr)); - } + return createScpCommandFactory(level, stdout, stderr, null); + } + + // SCP + DEFAULT SHELL + if (("+" + ScpCommandFactory.SCP_FACTORY_NAME).equalsIgnoreCase(factory)) { + return createScpCommandFactory(level, stdout, stderr, DEFAULT_SHELL_FACTORY); + } - return shell; + boolean useScp = false; + // SCP + CUSTOM SHELL + if (factory.startsWith(ScpCommandFactory.SCP_FACTORY_NAME + "+")) { + factory = factory.substring(ScpCommandFactory.SCP_FACTORY_NAME.length() + 1); + ValidateUtils.checkNotNullAndNotEmpty(factory, "No extra custom shell factory class specified"); + useScp = true; } ClassLoader cl = ThreadUtils.resolveDefaultClassLoader(ShellFactory.class); try { Class<?> clazz = cl.loadClass(factory); Object instance = clazz.newInstance(); - return ShellFactory.class.cast(instance); + ShellFactory shellFactory = ShellFactory.class.cast(instance); + return useScp ? createScpCommandFactory(level, stdout, stderr, shellFactory) : shellFactory; } catch (Exception e) { stderr.append("ERROR: Failed (").append(e.getClass().getSimpleName()).append(')') .append(" to instantiate shell factory=").append(factory) @@ -296,4 +307,16 @@ public abstract class SshServerCliSupport extends CliSupport { throw e; } } + + public static ScpCommandFactory createScpCommandFactory( + Level level, Appendable stdout, Appendable stderr, ShellFactory delegateShellFactory) { + ScpCommandFactory.Builder scp = new ScpCommandFactory.Builder() + .withDelegate(ProcessShellCommandFactory.INSTANCE) + .withDelegateShellFactory(delegateShellFactory); + if (isEnabledVerbosityLogging(level)) { + scp.addEventListener(new ScpCommandTransferEventListener(stdout, stderr)); + } + + return scp.build(); + } } diff --git a/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerMain.java b/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerMain.java index 0e437b5..27e48b6 100644 --- a/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerMain.java +++ b/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerMain.java @@ -29,7 +29,6 @@ import java.util.TreeMap; import java.util.logging.Level; import java.util.stream.Collectors; -import org.apache.sshd.cli.server.helper.ScpCommandTransferEventListener; import org.apache.sshd.common.NamedResource; import org.apache.sshd.common.PropertyResolver; import org.apache.sshd.common.PropertyResolverUtils; @@ -45,7 +44,6 @@ import org.apache.sshd.server.command.CommandFactory; import org.apache.sshd.server.config.SshServerConfigFileReader; import org.apache.sshd.server.config.keys.ServerIdentity; import org.apache.sshd.server.keyprovider.AbstractGeneratorHostKeyProvider; -import org.apache.sshd.server.shell.ProcessShellCommandFactory; import org.apache.sshd.server.shell.ShellFactory; import org.apache.sshd.server.subsystem.SubsystemFactory; @@ -222,12 +220,7 @@ public class SshServerMain extends SshServerCliSupport { if (shellFactory instanceof ScpCommandFactory) { scpFactory = (ScpCommandFactory) shellFactory; } else { - ScpCommandFactory.Builder builder = new ScpCommandFactory.Builder() - .withDelegate(ProcessShellCommandFactory.INSTANCE); - if (isEnabledVerbosityLogging(level)) { - builder = builder.addEventListener(new ScpCommandTransferEventListener(stdout, stderr)); - } - scpFactory = builder.build(); + scpFactory = createScpCommandFactory(level, stdout, stderr, null); } sshd.setCommandFactory(scpFactory); return scpFactory; diff --git a/sshd-core/src/main/java/org/apache/sshd/server/shell/AggregateShellFactory.java b/sshd-core/src/main/java/org/apache/sshd/server/shell/AggregateShellFactory.java new file mode 100644 index 0000000..0b839c4 --- /dev/null +++ b/sshd-core/src/main/java/org/apache/sshd/server/shell/AggregateShellFactory.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.sshd.server.shell; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.Objects; + +import org.apache.sshd.common.util.logging.AbstractLoggingBean; +import org.apache.sshd.server.channel.ChannelSession; +import org.apache.sshd.server.command.Command; + +/** + * Provides different shell(s) based on some criteria of the provided {@link ChannelSession} + * + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +public class AggregateShellFactory extends AbstractLoggingBean implements ShellFactory, ShellFactorySelector { + protected final ShellFactory defaultFactory; + protected final Collection<? extends ShellFactorySelector> selectors; + + /** + * @param selectors Selector {@link ShellFactorySelector}-s being consulted whether they wish to provide a + * {@link ShellFactory} for the provided {@link ChannelSession} argument. If a selector returns + * {@code null} then the next in line is consulted. If no match found then the default + * {@link InteractiveProcessShellFactory} is used + */ + public AggregateShellFactory( + Collection<? extends ShellFactorySelector> selectors) { + this(selectors, InteractiveProcessShellFactory.INSTANCE); + } + + /** + * @param selectors Selector {@link ShellFactorySelector}-s being consulted whether they wish to provide a + * {@link ShellFactory} for the provided {@link ChannelSession} argument. If a selector + * returns {@code null} then the next in line is consulted. + * @param defaultFactory The (mandatory) default {@link ShellFactory} to use if no selector matched + */ + public AggregateShellFactory( + Collection<? extends ShellFactorySelector> selectors, ShellFactory defaultFactory) { + this.selectors = (selectors == null) ? Collections.emptyList() : selectors; + this.defaultFactory = Objects.requireNonNull(defaultFactory, "No default factory provided"); + } + + @Override + public Command createShell(ChannelSession channel) throws IOException { + ShellFactory factory = selectShellFactory(channel); + if (factory == null) { + if (log.isDebugEnabled()) { + log.debug("createShell({}) using default factory={}", channel, defaultFactory); + } + + factory = defaultFactory; + } else { + if (log.isDebugEnabled()) { + log.debug("createShell({}) using selected factory={}", channel, factory); + } + } + + return factory.createShell(channel); + } + + @Override + public ShellFactory selectShellFactory(ChannelSession channel) throws IOException { + return ShellFactorySelector.selectShellFactory(selectors, channel); + } +} diff --git a/sshd-core/src/main/java/org/apache/sshd/server/shell/InvertedShellWrapper.java b/sshd-core/src/main/java/org/apache/sshd/server/shell/InvertedShellWrapper.java index bfcafa7..201f8c0 100644 --- a/sshd-core/src/main/java/org/apache/sshd/server/shell/InvertedShellWrapper.java +++ b/sshd-core/src/main/java/org/apache/sshd/server/shell/InvertedShellWrapper.java @@ -142,7 +142,6 @@ public class InvertedShellWrapper extends AbstractLoggingBean implements Command @Override public synchronized void destroy(ChannelSession channel) throws Exception { - boolean debugEnabled = log.isDebugEnabled(); Throwable err = null; try { shell.destroy(channel); diff --git a/sshd-core/src/main/java/org/apache/sshd/server/shell/ShellFactorySelector.java b/sshd-core/src/main/java/org/apache/sshd/server/shell/ShellFactorySelector.java new file mode 100644 index 0000000..b92d658 --- /dev/null +++ b/sshd-core/src/main/java/org/apache/sshd/server/shell/ShellFactorySelector.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.sshd.server.shell; + +import java.io.IOException; +import java.util.Collection; + +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.server.channel.ChannelSession; + +/** + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +@FunctionalInterface +public interface ShellFactorySelector { + /** + * + * @param channelSession The {@link ChannelSession} + * @return The {@link ShellFactory} to use for the channel - {@code null} if none + * @throws IOException If failed the selection + */ + ShellFactory selectShellFactory(ChannelSession channelSession) throws IOException; + + /** + * Consults each selector whether it wants to provide a factory for the {@link ChannelSession} + * + * @param selectors The {@link ShellFactorySelector}-s to consult - ignored if {@code null}/empty + * @param channel The {@link ChannelSession} instance + * @return The selected {@link ShellFactory} - {@code null} if no selector matched (in which case the + * default factory is used) + * @throws IOException if any selector threw it + */ + static ShellFactory selectShellFactory( + Collection<? extends ShellFactorySelector> selectors, ChannelSession channel) + throws IOException { + if (GenericUtils.isEmpty(selectors)) { + return null; + } + + for (ShellFactorySelector sel : selectors) { + ShellFactory factory = sel.selectShellFactory(channel); + if (factory != null) { + return factory; + } + } + + return null; + } +} diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/server/ScpCommandFactory.java b/sshd-scp/src/main/java/org/apache/sshd/scp/server/ScpCommandFactory.java index 35c6735..6e9b348 100644 --- a/sshd-scp/src/main/java/org/apache/sshd/scp/server/ScpCommandFactory.java +++ b/sshd-scp/src/main/java/org/apache/sshd/scp/server/ScpCommandFactory.java @@ -23,6 +23,7 @@ import java.util.Collection; import java.util.concurrent.CopyOnWriteArraySet; import java.util.function.Supplier; +import org.apache.sshd.common.session.SessionContext; import org.apache.sshd.common.util.EventListenerUtils; import org.apache.sshd.common.util.GenericUtils; import org.apache.sshd.common.util.ObjectBuilder; @@ -36,7 +37,9 @@ import org.apache.sshd.server.channel.ChannelSession; import org.apache.sshd.server.command.AbstractDelegatingCommandFactory; import org.apache.sshd.server.command.Command; import org.apache.sshd.server.command.CommandFactory; +import org.apache.sshd.server.shell.InteractiveProcessShellFactory; import org.apache.sshd.server.shell.ShellFactory; +import org.apache.sshd.server.shell.ShellFactorySelector; /** * This <code>CommandFactory</code> can be used as a standalone command factory or can be used to augment another @@ -48,7 +51,7 @@ import org.apache.sshd.server.shell.ShellFactory; */ public class ScpCommandFactory extends AbstractDelegatingCommandFactory - implements ManagedExecutorServiceSupplier, ScpFileOpenerHolder, Cloneable, ShellFactory { + implements ManagedExecutorServiceSupplier, ScpFileOpenerHolder, Cloneable, ShellFactory, ShellFactorySelector { public static final String SCP_FACTORY_NAME = "scp"; @@ -98,6 +101,11 @@ public class ScpCommandFactory return this; } + public Builder withDelegateShellFactory(ShellFactory shellFactory) { + factory.setDelegateShellFactory(shellFactory); + return this; + } + @Override public ScpCommandFactory build() { return factory.clone(); @@ -106,6 +114,7 @@ public class ScpCommandFactory private Supplier<? extends CloseableExecutorService> executorsProvider; private ScpFileOpener fileOpener; + private ShellFactory delegateShellFactory = InteractiveProcessShellFactory.INSTANCE; private int sendBufferSize = ScpHelper.DEFAULT_SEND_BUFFER_SIZE; private int receiveBufferSize = ScpHelper.DEFAULT_RECEIVE_BUFFER_SIZE; private Collection<ScpTransferEventListener> listeners = new CopyOnWriteArraySet<>(); @@ -219,13 +228,49 @@ public class ScpCommandFactory getScpFileOpener(), listenerProxy); } + /** + * @return The delegate {@link ShellFactory} to use if {@link #selectShellFactory(ChannelSession)} decides not to + * use itself as the {@link ShellFactory} - default={@link InteractiveProcessShellFactory}. + * @see #setDelegateShellFactory(ShellFactory) + */ + public ShellFactory getDelegateShellFactory() { + return delegateShellFactory; + } + + /** + * @param delegateShellFactory The {@link ShellFactory} to use if {@link #selectShellFactory(ChannelSession)} + * decides not to use itself as the {@link ShellFactory}. If {@code null} then it will + * always decide to use itself regardless of the {@link ChannelSession} + * @see #selectShellFactory(ChannelSession) + */ + public void setDelegateShellFactory(ShellFactory delegateShellFactory) { + this.delegateShellFactory = delegateShellFactory; + } + + @Override + public ShellFactory selectShellFactory(ChannelSession channelSession) throws IOException { + SessionContext session = channelSession.getSessionContext(); + String clientVersion = session.getClientVersion(); + // SSHD-1009 + if (clientVersion.contains("WinSCP")) { + return this; + } + + return delegateShellFactory; + } + @Override public Command createShell(ChannelSession channel) throws IOException { - return new ScpShell( - channel, - resolveExecutorService(), - getSendBufferSize(), getReceiveBufferSize(), - getScpFileOpener(), listenerProxy); + ShellFactory factory = selectShellFactory(channel); + if ((factory == this) || (factory == null)) { + return new ScpShell( + channel, + resolveExecutorService(), + getSendBufferSize(), getReceiveBufferSize(), + getScpFileOpener(), listenerProxy); + } else { + return factory.createShell(channel); + } } protected CloseableExecutorService resolveExecutorService(String command) {