| /* |
| * 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.commons.vfs2.provider.sftp; |
| |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.io.PrintStream; |
| import java.net.InetSocketAddress; |
| import java.net.Socket; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.TreeMap; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import org.apache.commons.AbstractVfsTestCase; |
| import org.apache.commons.vfs2.AbstractProviderTestConfig; |
| import org.apache.commons.vfs2.FileObject; |
| import org.apache.commons.vfs2.FileSystemManager; |
| import org.apache.commons.vfs2.FileSystemOptions; |
| import org.apache.commons.vfs2.ProviderTestSuite; |
| import org.apache.commons.vfs2.impl.DefaultFileSystemManager; |
| import org.apache.ftpserver.ftplet.FtpException; |
| import org.apache.sshd.SshServer; |
| import org.apache.sshd.common.NamedFactory; |
| import org.apache.sshd.common.Session; |
| import org.apache.sshd.common.SshException; |
| import org.apache.sshd.common.session.AbstractSession; |
| import org.apache.sshd.common.util.Buffer; |
| import org.apache.sshd.common.util.SecurityUtils; |
| import org.apache.sshd.server.Command; |
| import org.apache.sshd.server.Environment; |
| import org.apache.sshd.server.ExitCallback; |
| import org.apache.sshd.server.FileSystemFactory; |
| import org.apache.sshd.server.FileSystemView; |
| import org.apache.sshd.server.ForwardingFilter; |
| import org.apache.sshd.server.SshFile; |
| import org.apache.sshd.server.auth.UserAuthNone; |
| import org.apache.sshd.server.command.ScpCommandFactory; |
| import org.apache.sshd.server.filesystem.NativeSshFile; |
| import org.apache.sshd.server.keyprovider.PEMGeneratorHostKeyProvider; |
| import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; |
| import org.apache.sshd.server.session.ServerSession; |
| import org.apache.sshd.server.sftp.SftpSubsystem; |
| |
| import com.jcraft.jsch.SftpATTRS; |
| import com.jcraft.jsch.TestIdentityRepositoryFactory; |
| |
| /** |
| * Tests cases for the SFTP provider. |
| * <p> |
| * Starts and stops an embedded Apache SSHd (MINA) server. |
| * </p> |
| */ |
| abstract class AbstractSftpProviderTestCase extends AbstractProviderTestConfig { |
| |
| /** |
| * The underlying file system |
| */ |
| protected SftpFileSystem fileSystem; |
| |
| /** |
| * Implements FileSystemFactory because SSHd does not know about users and home directories. |
| */ |
| static final class TestFileSystemFactory implements FileSystemFactory { |
| /** |
| * Accepts only the known test user. |
| */ |
| @Override |
| public FileSystemView createFileSystemView(final Session session) throws IOException { |
| final String userName = session.getUsername(); |
| if (!DEFAULT_USER.equals(userName)) { |
| return null; |
| } |
| return new TestFileSystemView(AbstractVfsTestCase.getTestDirectory(), userName); |
| } |
| } |
| |
| /** |
| * Implements FileSystemView because SSHd does not know about users and home directories. |
| */ |
| static final class TestFileSystemView implements FileSystemView { |
| private final String homeDirStr; |
| |
| private final String userName; |
| |
| // private boolean caseInsensitive; |
| |
| public TestFileSystemView(final String homeDirStr, final String userName) { |
| this.homeDirStr = new File(homeDirStr).getAbsolutePath(); |
| this.userName = userName; |
| } |
| |
| @Override |
| public SshFile getFile(final SshFile baseDir, final String file) { |
| return this.getFile(baseDir.getAbsolutePath(), file); |
| } |
| |
| @Override |
| public SshFile getFile(final String file) { |
| return this.getFile(homeDirStr, file); |
| } |
| |
| protected SshFile getFile(final String dir, final String file) { |
| final String home = removePrefix(NativeSshFile.normalizeSeparateChar(homeDirStr)); |
| String userFileName = removePrefix(NativeSshFile.normalizeSeparateChar(file)); |
| final File sshFile = userFileName.startsWith(home) ? new File(userFileName) : new File(home, userFileName); |
| userFileName = removePrefix(NativeSshFile.normalizeSeparateChar(sshFile.getAbsolutePath())); |
| return new TestNativeSshFile(userFileName, sshFile, userName); |
| } |
| |
| private String removePrefix(final String s) { |
| final int index = s.indexOf('/'); |
| if (index < 1) { |
| return s; |
| } |
| return s.substring(index); |
| } |
| } |
| |
| /** |
| * Extends NativeSshFile because its constructor is protected and I do not want to create a whole NativeSshFile |
| * implementation for testing. |
| */ |
| static class TestNativeSshFile extends NativeSshFile { |
| TestNativeSshFile(final String fileName, final File file, final String userName) { |
| super(fileName, file, userName); |
| } |
| } |
| |
| private static int SocketPort; |
| |
| private static final String DEFAULT_USER = "testtest"; |
| |
| // private static final String DEFAULT_PWD = "testtest"; |
| |
| protected static String ConnectionUri; |
| |
| private static SshServer Server; |
| |
| private static final String TEST_URI = "test.sftp.uri"; |
| |
| /** |
| * True if we are testing the SFTP stream proxy |
| */ |
| protected static String getSystemTestUriOverride() { |
| return System.getProperty(TEST_URI); |
| } |
| |
| /** |
| * Creates and starts an embedded Apache SSHd Server (MINA). |
| * |
| * @throws FtpException |
| * @throws IOException |
| */ |
| private static void setUpClass(final boolean isExecChannelClosed) throws IOException { |
| if (Server != null) { |
| return; |
| } |
| // System.setProperty("vfs.sftp.sshdir", getTestDirectory() + "/../vfs.sftp.sshdir"); |
| final String tmpDir = System.getProperty("java.io.tmpdir"); |
| Server = SshServer.setUpDefaultServer(); |
| Server.setPort(0); |
| if (SecurityUtils.isBouncyCastleRegistered()) { |
| // A temporary file will hold the key |
| final File keyFile = File.createTempFile("key", ".pem", new File(tmpDir)); |
| keyFile.deleteOnExit(); |
| // It has to be deleted in order to be generated |
| keyFile.delete(); |
| |
| final PEMGeneratorHostKeyProvider keyProvider = new PEMGeneratorHostKeyProvider(keyFile.getAbsolutePath()); |
| Server.setKeyPairProvider(keyProvider); |
| } else { |
| Server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(tmpDir + "/key.ser")); |
| } |
| final List<NamedFactory<Command>> list = new ArrayList<>(1); |
| list.add(new NamedFactory<Command>() { |
| |
| @Override |
| public String getName() { |
| return "sftp"; |
| } |
| |
| @Override |
| public Command create() { |
| return new MySftpSubsystem(); |
| } |
| }); |
| Server.setSubsystemFactories(list); |
| Server.setPasswordAuthenticator((username, password, session) -> username != null && username.equals(password)); |
| Server.setPublickeyAuthenticator((username, key, session) -> true); |
| Server.setForwardingFilter(new ForwardingFilter() { |
| @Override |
| public boolean canConnect(final InetSocketAddress address, final ServerSession session) { |
| return true; |
| } |
| |
| @Override |
| public boolean canForwardAgent(final ServerSession session) { |
| return true; |
| } |
| |
| @Override |
| public boolean canForwardX11(final ServerSession session) { |
| return true; |
| } |
| |
| @Override |
| public boolean canListen(final InetSocketAddress address, final ServerSession session) { |
| return true; |
| } |
| }); |
| // Allows the execution of commands |
| Server.setCommandFactory(new ScpCommandFactory(new TestCommandFactory(isExecChannelClosed))); |
| // HACK Start |
| // How do we really do simple user to directory matching? |
| Server.setFileSystemFactory(new TestFileSystemFactory()); |
| // HACK End |
| Server.start(); |
| SocketPort = Server.getPort(); |
| ConnectionUri = String.format("sftp://%s@localhost:%d", DEFAULT_USER, SocketPort); |
| // HACK Start |
| // How do we really do simple security? |
| // Do this after we start the server to simplify this set up code. |
| Server.getUserAuthFactories().add(new UserAuthNone.Factory()); |
| // HACK End |
| } |
| |
| static class SftpProviderTestSuite extends ProviderTestSuite { |
| private final boolean isExecChannelClosed; |
| |
| public SftpProviderTestSuite(final AbstractSftpProviderTestCase providerConfig) throws Exception { |
| super(providerConfig); |
| this.isExecChannelClosed = providerConfig.isExecChannelClosed(); |
| } |
| |
| @Override |
| protected void setUp() throws Exception { |
| if (getSystemTestUriOverride() == null) { |
| setUpClass(isExecChannelClosed); |
| } |
| super.setUp(); |
| } |
| |
| @Override |
| protected void tearDown() throws Exception { |
| // Close all active sessions |
| // Note that it should be done by super.tearDown() |
| // while closing |
| for (final AbstractSession session : Server.getActiveSessions()) { |
| session.close(true); |
| } |
| tearDownClass(); |
| super.tearDown(); |
| } |
| |
| } |
| |
| protected abstract boolean isExecChannelClosed(); |
| |
| /** |
| * Stops the embedded Apache SSHd Server (MINA). |
| * |
| * @throws InterruptedException |
| */ |
| private static void tearDownClass() throws InterruptedException { |
| if (Server != null) { |
| Server.stop(); |
| Server = null; |
| } |
| } |
| |
| /** |
| * Returns the base folder for tests. |
| */ |
| @Override |
| public FileObject getBaseTestFolder(final FileSystemManager manager) throws Exception { |
| String uri = getSystemTestUriOverride(); |
| if (uri == null) { |
| uri = ConnectionUri; |
| } |
| |
| final FileSystemOptions fileSystemOptions = new FileSystemOptions(); |
| final SftpFileSystemConfigBuilder builder = SftpFileSystemConfigBuilder.getInstance(); |
| builder.setStrictHostKeyChecking(fileSystemOptions, "no"); |
| builder.setUserInfo(fileSystemOptions, new TrustEveryoneUserInfo()); |
| builder.setIdentityRepositoryFactory(fileSystemOptions, new TestIdentityRepositoryFactory()); |
| final FileObject fileObject = manager.resolveFile(uri, fileSystemOptions); |
| this.fileSystem = (SftpFileSystem) fileObject.getFileSystem(); |
| return fileObject; |
| } |
| |
| /** |
| * Prepares the file system manager. |
| */ |
| @Override |
| public void prepare(final DefaultFileSystemManager manager) throws Exception { |
| manager.addProvider("sftp", new SftpFileProvider()); |
| } |
| |
| /** |
| * The command factory for the SSH server: Handles these commands |
| * <p> |
| * <li><code>id -u</code> (permissions test)</li> |
| * <li><code>id -G</code> (permission tests)</li> |
| * <li><code>nc -q 0 localhost port</code> (Stream proxy tests)</li> |
| * </p> |
| */ |
| private static class TestCommandFactory extends ScpCommandFactory { |
| |
| public static final Pattern NETCAT_COMMAND = Pattern.compile("nc -q 0 localhost (\\d+)"); |
| private final boolean isExecChannelClosed; |
| |
| public TestCommandFactory(final boolean isExecChannelClosed) { |
| this.isExecChannelClosed = isExecChannelClosed; |
| } |
| |
| @Override |
| public Command createCommand(final String command) { |
| return new Command() { |
| public ExitCallback callback; |
| public OutputStream out; |
| public OutputStream err; |
| public InputStream in; |
| |
| @Override |
| public void setInputStream(final InputStream in) { |
| this.in = in; |
| } |
| |
| @Override |
| public void setOutputStream(final OutputStream out) { |
| this.out = out; |
| } |
| |
| @Override |
| public void setErrorStream(final OutputStream err) { |
| this.err = err; |
| } |
| |
| @Override |
| public void setExitCallback(final ExitCallback callback) { |
| this.callback = callback; |
| |
| } |
| |
| @Override |
| public void start(final Environment env) throws IOException { |
| int code = 0; |
| if (command.equals("id -G") || command.equals("id -u")) { |
| if (isExecChannelClosed) { |
| throw new IOException("TestingExecChannelClosed"); |
| } |
| new PrintStream(out).println(0); |
| } else if (NETCAT_COMMAND.matcher(command).matches()) { |
| final Matcher matcher = NETCAT_COMMAND.matcher(command); |
| matcher.matches(); |
| final int port = Integer.parseInt(matcher.group(1)); |
| |
| final Socket socket = new Socket((String) null, port); |
| |
| if (out != null) { |
| connect("from nc", socket.getInputStream(), out, null); |
| } |
| |
| if (in != null) { |
| connect("to nc", in, socket.getOutputStream(), callback); |
| } |
| |
| return; |
| |
| } else { |
| if (err != null) { |
| new PrintStream(err).format("Unknown command %s%n", command); |
| } |
| code = -1; |
| } |
| |
| if (out != null) { |
| out.flush(); |
| } |
| if (err != null) { |
| err.flush(); |
| } |
| callback.onExit(code); |
| } |
| |
| @Override |
| public void destroy() { |
| // empty |
| } |
| }; |
| } |
| } |
| |
| /** |
| * Creates a pipe thread that connects an input to an output |
| * |
| * @param name The name of the thread (for debugging purposes) |
| * @param in The input stream |
| * @param out The output stream |
| * @param callback An object whose method {@linkplain ExitCallback#onExit(int)} will be called when the pipe is |
| * broken. The integer argument is 0 if everything went well. |
| */ |
| private static void connect(final String name, final InputStream in, final OutputStream out, |
| final ExitCallback callback) { |
| final Thread thread = new Thread((Runnable) () -> { |
| int code = 0; |
| try { |
| final byte buffer[] = new byte[1024]; |
| int len; |
| while ((len = in.read(buffer, 0, buffer.length)) != -1) { |
| out.write(buffer, 0, len); |
| out.flush(); |
| } |
| } catch (final SshException ex1) { |
| // Nothing to do, this occurs when the connection |
| // is closed on the remote side |
| } catch (final IOException ex2) { |
| if (!ex2.getMessage().equals("Pipe closed")) { |
| code = -1; |
| } |
| } |
| if (callback != null) { |
| callback.onExit(code); |
| } |
| }, name); |
| thread.setDaemon(true); |
| thread.start(); |
| } |
| |
| private static class SftpAttrs { |
| int flags; |
| private int uid; |
| long size; |
| private int gid; |
| private int atime; |
| private int permissions; |
| private int mtime; |
| private String[] extended; |
| |
| private SftpAttrs(final Buffer buf) { |
| final int flags = buf.getInt(); |
| |
| if ((flags & SftpATTRS.SSH_FILEXFER_ATTR_SIZE) != 0) { |
| size = buf.getLong(); |
| } |
| if ((flags & SftpATTRS.SSH_FILEXFER_ATTR_UIDGID) != 0) { |
| uid = buf.getInt(); |
| gid = buf.getInt(); |
| } |
| if ((flags & SftpATTRS.SSH_FILEXFER_ATTR_PERMISSIONS) != 0) { |
| permissions = buf.getInt(); |
| } |
| if ((flags & SftpATTRS.SSH_FILEXFER_ATTR_ACMODTIME) != 0) { |
| atime = buf.getInt(); |
| } |
| if ((flags & SftpATTRS.SSH_FILEXFER_ATTR_ACMODTIME) != 0) { |
| mtime = buf.getInt(); |
| } |
| |
| } |
| } |
| |
| private static class MySftpSubsystem extends SftpSubsystem { |
| TreeMap<String, Integer> permissions = new TreeMap<>(); |
| private int _version; |
| |
| @Override |
| protected void process(final Buffer buffer) throws IOException { |
| final int rpos = buffer.rpos(); |
| final int length = buffer.getInt(); |
| final int type = buffer.getByte(); |
| final int id = buffer.getInt(); |
| |
| switch (type) { |
| case SSH_FXP_SETSTAT: |
| case SSH_FXP_FSETSTAT: { |
| // Get the path |
| final String path = buffer.getString(); |
| // Get the permission |
| final SftpAttrs attrs = new SftpAttrs(buffer); |
| permissions.put(path, attrs.permissions); |
| // System.err.format("Setting [%s] permission to %o%n", path, attrs.permissions); |
| break; |
| } |
| |
| case SSH_FXP_REMOVE: { |
| // Remove cached attributes |
| final String path = buffer.getString(); |
| permissions.remove(path); |
| // System.err.format("Removing [%s] permission cache%n", path); |
| break; |
| } |
| |
| case SSH_FXP_INIT: { |
| // Just grab the version here |
| this._version = id; |
| break; |
| } |
| } |
| |
| buffer.rpos(rpos); |
| super.process(buffer); |
| |
| } |
| |
| @Override |
| protected void writeAttrs(final Buffer buffer, final SshFile file, final int flags) throws IOException { |
| if (!file.doesExist()) { |
| throw new FileNotFoundException(file.getAbsolutePath()); |
| } |
| |
| int p = 0; |
| |
| final Integer cached = permissions.get(file.getAbsolutePath()); |
| if (cached != null) { |
| // Use cached permissions |
| // System.err.format("Using cached [%s] permission of %o%n", file.getAbsolutePath(), cached); |
| p |= cached; |
| } else { |
| // Use permissions from Java file |
| if (file.isReadable()) { |
| p |= S_IRUSR; |
| } |
| if (file.isWritable()) { |
| p |= S_IWUSR; |
| } |
| if (file.isExecutable()) { |
| p |= S_IXUSR; |
| } |
| } |
| |
| if (_version >= 4) { |
| final long size = file.getSize(); |
| // String username = session.getUsername(); |
| final long lastModif = file.getLastModified(); |
| if (file.isFile()) { |
| buffer.putInt(SSH_FILEXFER_ATTR_PERMISSIONS); |
| buffer.putByte((byte) SSH_FILEXFER_TYPE_REGULAR); |
| buffer.putInt(p); |
| } else if (file.isDirectory()) { |
| buffer.putInt(SSH_FILEXFER_ATTR_PERMISSIONS); |
| buffer.putByte((byte) SSH_FILEXFER_TYPE_DIRECTORY); |
| buffer.putInt(p); |
| } else { |
| buffer.putInt(0); |
| buffer.putByte((byte) SSH_FILEXFER_TYPE_UNKNOWN); |
| } |
| } else { |
| if (file.isFile()) { |
| p |= 0100000; |
| } |
| if (file.isDirectory()) { |
| p |= 0040000; |
| } |
| |
| if (file.isFile()) { |
| buffer.putInt(SSH_FILEXFER_ATTR_SIZE | SSH_FILEXFER_ATTR_PERMISSIONS | SSH_FILEXFER_ATTR_ACMODTIME); |
| buffer.putLong(file.getSize()); |
| buffer.putInt(p); |
| buffer.putInt(file.getLastModified() / 1000); |
| buffer.putInt(file.getLastModified() / 1000); |
| } else if (file.isDirectory()) { |
| buffer.putInt(SSH_FILEXFER_ATTR_PERMISSIONS | SSH_FILEXFER_ATTR_ACMODTIME); |
| buffer.putInt(p); |
| buffer.putInt(file.getLastModified() / 1000); |
| buffer.putInt(file.getLastModified() / 1000); |
| } else { |
| buffer.putInt(0); |
| } |
| } |
| } |
| |
| } |
| } |