http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/251db9b9/sshd-sftp/src/test/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystemFactoryTest.java ---------------------------------------------------------------------- diff --git a/sshd-sftp/src/test/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystemFactoryTest.java b/sshd-sftp/src/test/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystemFactoryTest.java new file mode 100644 index 0000000..6420411 --- /dev/null +++ b/sshd-sftp/src/test/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystemFactoryTest.java @@ -0,0 +1,101 @@ +/* + * 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.subsystem.sftp; + +import java.util.concurrent.ExecutorService; + +import org.apache.sshd.util.test.BaseTestSupport; +import org.apache.sshd.util.test.NoIoTestCase; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runners.MethodSorters; +import org.mockito.Mockito; + +/** + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Category({ NoIoTestCase.class }) +public class SftpSubsystemFactoryTest extends BaseTestSupport { + public SftpSubsystemFactoryTest() { + super(); + } + + /** + * Make sure that the builder returns a factory with the default values + * if no {@code withXXX} method is invoked + */ + @Test + public void testBuilderDefaultFactoryValues() { + SftpSubsystemFactory factory = new SftpSubsystemFactory.Builder().build(); + assertNull("Mismatched executor", factory.getExecutorService()); + assertFalse("Mismatched shutdown state", factory.isShutdownOnExit()); + assertSame("Mismatched unsupported attribute policy", SftpSubsystemFactory.DEFAULT_POLICY, factory.getUnsupportedAttributePolicy()); + } + + /** + * Make sure that the builder initializes correctly the built factory + */ + @Test + public void testBuilderCorrectlyInitializesFactory() { + SftpSubsystemFactory.Builder builder = new SftpSubsystemFactory.Builder(); + ExecutorService service = dummyExecutor(); + SftpSubsystemFactory factory = builder.withExecutorService(service) + .withShutdownOnExit(true) + .build(); + assertSame("Mismatched executor", service, factory.getExecutorService()); + assertTrue("Mismatched shutdown state", factory.isShutdownOnExit()); + + for (UnsupportedAttributePolicy policy : UnsupportedAttributePolicy.VALUES) { + SftpSubsystemFactory actual = builder.withUnsupportedAttributePolicy(policy).build(); + assertSame("Mismatched unsupported attribute policy", policy, actual.getUnsupportedAttributePolicy()); + } + } + + /** + * <UL> + * <LI> + * Make sure the builder returns new instances on every call to + * {@link SftpSubsystemFactory.Builder#build()} method + * </LI> + * + * <LI> + * Make sure values are preserved between successive invocations + * of the {@link SftpSubsystemFactory.Builder#build()} method + * </LI> + * </UL + */ + @Test + public void testBuilderUniqueInstance() { + SftpSubsystemFactory.Builder builder = new SftpSubsystemFactory.Builder(); + SftpSubsystemFactory f1 = builder.withExecutorService(dummyExecutor()).build(); + SftpSubsystemFactory f2 = builder.build(); + assertNotSame("No new instance built", f1, f2); + assertSame("Mismatched executors", f1.getExecutorService(), f2.getExecutorService()); + + SftpSubsystemFactory f3 = builder.withExecutorService(dummyExecutor()).build(); + assertNotSame("Executor service not changed", f1.getExecutorService(), f3.getExecutorService()); + } + + private static ExecutorService dummyExecutor() { + return Mockito.mock(ExecutorService.class); + } +}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/251db9b9/sshd-sftp/src/test/java/org/apache/sshd/server/subsystem/sftp/SshFsMounter.java ---------------------------------------------------------------------- diff --git a/sshd-sftp/src/test/java/org/apache/sshd/server/subsystem/sftp/SshFsMounter.java b/sshd-sftp/src/test/java/org/apache/sshd/server/subsystem/sftp/SshFsMounter.java new file mode 100644 index 0000000..e6b10e0 --- /dev/null +++ b/sshd-sftp/src/test/java/org/apache/sshd/server/subsystem/sftp/SshFsMounter.java @@ -0,0 +1,327 @@ +/* + * 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.subsystem.sftp; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; + +import org.apache.sshd.common.PropertyResolver; +import org.apache.sshd.common.PropertyResolverUtils; +import org.apache.sshd.common.config.SshConfigFileReader; +import org.apache.sshd.common.io.BuiltinIoServiceFactoryFactories; +import org.apache.sshd.common.io.IoServiceFactory; +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.ValidateUtils; +import org.apache.sshd.common.util.logging.AbstractLoggingBean; +import org.apache.sshd.common.util.security.SecurityUtils; +import org.apache.sshd.common.util.threads.ThreadUtils; +import org.apache.sshd.server.Command; +import org.apache.sshd.server.CommandFactory; +import org.apache.sshd.server.Environment; +import org.apache.sshd.server.ExitCallback; +import org.apache.sshd.server.SessionAware; +import org.apache.sshd.server.SshServer; +import org.apache.sshd.server.auth.password.AcceptAllPasswordAuthenticator; +import org.apache.sshd.server.forward.AcceptAllForwardingFilter; +import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; +import org.apache.sshd.server.scp.ScpCommandFactory; +import org.apache.sshd.server.session.ServerSession; +import org.apache.sshd.server.shell.InteractiveProcessShellFactory; +import org.apache.sshd.util.test.Utils; + +/** + * A basic implementation to allow remote mounting of the local file system via SFTP + * + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +public final class SshFsMounter { + public static class MounterCommand extends AbstractLoggingBean implements Command, SessionAware, Runnable { + private final String command; + private final String cmdName; + private final List<String> args; + private String username; + private InputStream stdin; + private PrintStream stdout; + private PrintStream stderr; + private ExitCallback callback; + private ExecutorService executor; + private Future<?> future; + + public MounterCommand(String command) { + this.command = ValidateUtils.checkNotNullAndNotEmpty(command, "No command"); + + String[] comps = GenericUtils.split(this.command, ' '); + int numComps = GenericUtils.length(comps); + cmdName = GenericUtils.trimToEmpty(ValidateUtils.checkNotNullAndNotEmpty(comps[0], "No command name")); + if (numComps > 1) { + args = new ArrayList<>(numComps - 1); + for (int index = 1; index < numComps; index++) { + String c = GenericUtils.trimToEmpty(comps[index]); + if (GenericUtils.isEmpty(c)) { + continue; + } + + args.add(c); + } + } else { + args = Collections.emptyList(); + } + + log.info("<init>(" + command + ")"); + } + + @Override + public void run() { + try { + log.info("run(" + username + ")[" + command + "] start"); + if ("id".equals(cmdName)) { + int numArgs = GenericUtils.size(args); + if (numArgs <= 0) { + stdout.println("uid=0(root) gid=0(root) groups=0(root)"); + } else if (numArgs == 1) { + String modifier = args.get(0); + if ("-u".equals(modifier) || "-G".equals(modifier)) { + stdout.println("0"); + } else { + throw new IllegalArgumentException("Unknown modifier: " + modifier); + } + } else { + throw new IllegalArgumentException("Unexpected extra command arguments"); + } + } else { + throw new UnsupportedOperationException("Unknown command"); + } + + log.info("run(" + username + ")[" + command + "] end"); + callback.onExit(0); + } catch (Exception e) { + log.error("run(" + username + ")[" + command + "] " + e.getClass().getSimpleName() + ": " + e.getMessage(), e); + stderr.append(e.getClass().getSimpleName()).append(": ").println(e.getMessage()); + callback.onExit(-1, e.toString()); + } + } + + @Override + public void setSession(ServerSession session) { + username = session.getUsername(); + } + + @Override + public void setInputStream(InputStream in) { + this.stdin = in; + } + + @Override + public void setOutputStream(OutputStream out) { + this.stdout = new PrintStream(out, true); + } + + @Override + public void setErrorStream(OutputStream err) { + this.stderr = new PrintStream(err, true); + } + + @Override + public void setExitCallback(ExitCallback callback) { + this.callback = callback; + } + + @Override + public void start(Environment env) throws IOException { + executor = ThreadUtils.newSingleThreadExecutor(getClass().getSimpleName()); + future = executor.submit(this); + } + + @Override + public void destroy() { + stopCommand(); + + if (stdout != null) { + try { + log.info("destroy(" + username + ")[" + command + "] close stdout"); + stdout.close(); + log.info("destroy(" + username + ")[" + command + "] stdout closed"); + } finally { + stdout = null; + } + } + + if (stderr != null) { + try { + log.info("destroy(" + username + ")[" + command + "] close stderr"); + stderr.close(); + log.info("destroy(" + username + ")[" + command + "] stderr closed"); + } finally { + stderr = null; + } + } + + if (stdin != null) { + try { + log.info("destroy(" + username + ")[" + command + "] close stdin"); + stdin.close(); + log.info("destroy(" + username + ")[" + command + "] stdin closed"); + } catch (IOException e) { + log.warn("destroy(" + username + ")[" + command + "] failed (" + e.getClass().getSimpleName() + ") to close stdin: " + e.getMessage()); + if (log.isDebugEnabled()) { + log.debug("destroy(" + username + ")[" + command + "] failure details", e); + } + } finally { + stdin = null; + } + } + } + + private void stopCommand() { + if ((future != null) && (!future.isDone())) { + try { + log.info("stopCommand(" + username + ")[" + command + "] cancelling"); + future.cancel(true); + log.info("stopCommand(" + username + ")[" + command + "] cancelled"); + } finally { + future = null; + } + } + + if ((executor != null) && (!executor.isShutdown())) { + try { + log.info("stopCommand(" + username + ")[" + command + "] shutdown executor"); + executor.shutdownNow(); + log.info("stopCommand(" + username + ")[" + command + "] executor shut down"); + } finally { + executor = null; + } + } + } + } + + public static class MounterCommandFactory implements CommandFactory { + public static final MounterCommandFactory INSTANCE = new MounterCommandFactory(); + + public MounterCommandFactory() { + super(); + } + + @Override + public Command createCommand(String command) { + return new MounterCommand(command); + } + } + + private SshFsMounter() { + throw new UnsupportedOperationException("No instance"); + } + + ////////////////////////////////////////////////////////////////////////// + + public static void main(String[] args) throws Exception { + int port = SshConfigFileReader.DEFAULT_PORT; + boolean error = false; + Map<String, Object> options = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + int numArgs = GenericUtils.length(args); + for (int i = 0; i < numArgs; i++) { + String argName = args[i]; + if ("-p".equals(argName)) { + if ((i + 1) >= numArgs) { + System.err.println("option requires an argument: " + argName); + break; + } + port = Integer.parseInt(args[++i]); + } else if ("-io".equals(argName)) { + if (i + 1 >= numArgs) { + System.err.println("option requires an argument: " + argName); + break; + } + + String provider = args[++i]; + if ("mina".equals(provider)) { + System.setProperty(IoServiceFactory.class.getName(), BuiltinIoServiceFactoryFactories.MINA.getFactoryClassName()); + } else if ("nio2".endsWith(provider)) { + System.setProperty(IoServiceFactory.class.getName(), BuiltinIoServiceFactoryFactories.NIO2.getFactoryClassName()); + } else { + System.err.println("provider should be mina or nio2: " + argName); + error = true; + break; + } + } else if ("-o".equals(argName)) { + if ((i + 1) >= numArgs) { + System.err.println("option requires and argument: " + argName); + error = true; + break; + } + String opt = args[++i]; + int idx = opt.indexOf('='); + if (idx <= 0) { + System.err.println("bad syntax for option: " + opt); + error = true; + break; + } + options.put(opt.substring(0, idx), opt.substring(idx + 1)); + } else if (argName.startsWith("-")) { + System.err.println("illegal option: " + argName); + error = true; + break; + } else { + System.err.println("extra argument: " + argName); + error = true; + break; + } + } + if (error) { + System.err.println("usage: sshfs [-p port] [-io mina|nio2] [-o option=value]"); + System.exit(-1); + } + + SshServer sshd = Utils.setupTestServer(SshFsMounter.class); + Map<String, Object> props = sshd.getProperties(); + props.putAll(options); + PropertyResolver resolver = PropertyResolverUtils.toPropertyResolver(options); + File targetFolder = Objects.requireNonNull(Utils.detectTargetFolder(MounterCommandFactory.class), "Failed to detect target folder"); + if (SecurityUtils.isBouncyCastleRegistered()) { + sshd.setKeyPairProvider(SecurityUtils.createGeneratorHostKeyProvider(new File(targetFolder, "key.pem").toPath())); + } else { + sshd.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(new File(targetFolder, "key.ser"))); + } + // Should come AFTER key pair provider setup so auto-welcome can be generated if needed + SshServer.setupServerBanner(sshd, resolver); + + sshd.setShellFactory(InteractiveProcessShellFactory.INSTANCE); + sshd.setPasswordAuthenticator(AcceptAllPasswordAuthenticator.INSTANCE); + sshd.setForwardingFilter(AcceptAllForwardingFilter.INSTANCE); + sshd.setCommandFactory(new ScpCommandFactory.Builder().withDelegate(MounterCommandFactory.INSTANCE).build()); + sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory())); + sshd.setPort(port); + + System.err.println("Starting SSHD on port " + port); + sshd.start(); + Thread.sleep(Long.MAX_VALUE); + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/251db9b9/sshd-spring-sftp/pom.xml ---------------------------------------------------------------------- diff --git a/sshd-spring-sftp/pom.xml b/sshd-spring-sftp/pom.xml index 2f9e442..01cbf6a 100644 --- a/sshd-spring-sftp/pom.xml +++ b/sshd-spring-sftp/pom.xml @@ -55,6 +55,11 @@ <artifactId>sshd-core</artifactId> <version>${project.version}</version> </dependency> + <dependency> + <groupId>org.apache.sshd</groupId> + <artifactId>sshd-sftp</artifactId> + <version>${project.version}</version> + </dependency> <!-- Replacement of commons-logging for Spring parts that still use it --> <dependency> <groupId>org.slf4j</groupId> http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/251db9b9/sshd-spring-sftp/src/main/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactory.java ---------------------------------------------------------------------- diff --git a/sshd-spring-sftp/src/main/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactory.java b/sshd-spring-sftp/src/main/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactory.java index 85aecec..d5a76a7 100644 --- a/sshd-spring-sftp/src/main/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactory.java +++ b/sshd-spring-sftp/src/main/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactory.java @@ -34,6 +34,7 @@ import org.apache.sshd.client.session.ClientSession; import org.apache.sshd.client.simple.SimpleClientConfigurator; import org.apache.sshd.client.subsystem.sftp.SftpClient; import org.apache.sshd.client.subsystem.sftp.SftpClient.DirEntry; +import org.apache.sshd.client.subsystem.sftp.SftpClientFactory; import org.apache.sshd.client.subsystem.sftp.SftpVersionSelector; import org.apache.sshd.common.PropertyResolverUtils; import org.apache.sshd.common.config.SshConfigFileReader; @@ -338,7 +339,7 @@ public class ApacheSshdSftpSessionFactory session = resolveClientSession(sharedInstance); SftpVersionSelector selector = getSftpVersionSelector(); - SftpClient sftpClient = session.createSftpClient(selector); + SftpClient sftpClient = SftpClientFactory.instance().createSftpClient(session, selector); try { ClientSession sessionInstance = session; Session<DirEntry> result = sharedInstance