/* * 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.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import org.apache.sshd.common.config.SshConfigFileReader; import org.apache.sshd.common.io.IoServiceFactory; import org.apache.sshd.common.io.mina.MinaServiceFactory; import org.apache.sshd.common.io.nio2.Nio2ServiceFactory; 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:dev@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, String> options = new LinkedHashMap<>(); 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(), MinaServiceFactory.class.getName()); } else if ("nio2".endsWith(provider)) { System.setProperty(IoServiceFactory.class.getName(), Nio2ServiceFactory.class.getName()); } 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); } System.err.println("Starting SSHD on port " + port); SshServer sshd = Utils.setupTestServer(SshFsMounter.class); Map<String, Object> props = sshd.getProperties(); // FactoryManagerUtils.updateProperty(props, ServerFactoryManager.WELCOME_BANNER, "Welcome to SSH-FS Mounter\n"); props.putAll(options); sshd.setPort(port); 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"))); } sshd.setShellFactory(InteractiveProcessShellFactory.INSTANCE); sshd.setPasswordAuthenticator(AcceptAllPasswordAuthenticator.INSTANCE); sshd.setTcpipForwardingFilter(AcceptAllForwardingFilter.INSTANCE); sshd.setCommandFactory(new ScpCommandFactory.Builder().withDelegate(MounterCommandFactory.INSTANCE).build()); sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory())); sshd.start(); Thread.sleep(Long.MAX_VALUE); } }