blob: bf6480d3aaf5ef4e983156a7cef4d92a3bc8990c [file] [log] [blame]
/*
* 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.test;
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.net.URI;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.List;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import junit.extensions.TestSetup;
import junit.framework.Test;
import junit.framework.TestSuite;
import org.apache.commons.AbstractVfsTestCase;
import org.apache.commons.vfs2.FileObject;
import org.apache.commons.vfs2.FileSystemManager;
import org.apache.commons.vfs2.FileSystemOptions;
import org.apache.commons.vfs2.impl.DefaultFileSystemManager;
import org.apache.commons.vfs2.provider.sftp.SftpFileProvider;
import org.apache.commons.vfs2.provider.sftp.SftpFileSystem;
import org.apache.commons.vfs2.provider.sftp.SftpFileSystemConfigBuilder;
import org.apache.commons.vfs2.provider.sftp.SftpStreamProxy;
import org.apache.commons.vfs2.provider.sftp.TrustEveryoneUserInfo;
import org.apache.commons.vfs2.test.AbstractProviderTestConfig;
import org.apache.commons.vfs2.test.PermissionsTests;
import org.apache.commons.vfs2.test.ProviderReadTests;
import org.apache.commons.vfs2.test.ProviderTestConfig;
import org.apache.commons.vfs2.test.ProviderTestSuite;
import org.apache.commons.vfs2.util.FreeSocketPortUtil;
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.PasswordAuthenticator;
import org.apache.sshd.server.PublickeyAuthenticator;
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>
*/
public class SftpProviderTestCase extends AbstractProviderTestConfig
{
/**
* The underlying filesystem
*/
private 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";
private 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 */
private final boolean streamProxyMode;
private 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() throws FtpException, IOException, InterruptedException
{
SocketPort = FreeSocketPortUtil.findFreeLocalPort();
// Use %40 for @ in a URL
ConnectionUri = String.format("sftp://%s@localhost:%d", DEFAULT_USER, SocketPort);
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(SocketPort);
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<NamedFactory<Command>>(1);
list.add(new NamedFactory<Command>()
{
@Override
public String getName()
{
return "sftp";
}
@Override
public Command create()
{
return new MySftpSubsystem();
}
});
Server.setSubsystemFactories(list);
Server.setPasswordAuthenticator(new PasswordAuthenticator()
{
@Override
public boolean authenticate(final String username, final String password, final ServerSession session)
{
return username != null && username.equals(password);
}
});
Server.setPublickeyAuthenticator(new PublickeyAuthenticator()
{
@Override
public boolean authenticate(final String username, final PublicKey key, final ServerSession session)
{
// File f = new File("/Users/" + username + "/.ssh/authorized_keys");
return 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()));
// HACK Start
// How do we really do simple user to directory matching?
Server.setFileSystemFactory(new TestFileSystemFactory());
// HACK End
Server.start();
// 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 private class BaseProviderTestSuite extends ProviderTestSuite {
public BaseProviderTestSuite(final ProviderTestConfig providerConfig) throws Exception
{
super(providerConfig);
}
@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);
}
super.tearDown();
}
}
/**
* Creates the test suite for the ftp file system.
*/
public static Test suite() throws Exception
{
// The test suite to be returned
final TestSuite suite = new TestSuite();
// --- Standard VFS test suite
final SftpProviderTestCase standardTestCase = new SftpProviderTestCase(false);
final ProviderTestSuite sftpSuite = new BaseProviderTestSuite(standardTestCase);
// VFS-405: set/get permissions
sftpSuite.addTests(PermissionsTests.class);
suite.addTest(sftpSuite);
// --- VFS-440: stream proxy test suite
// We override the addBaseTests method so that only
// one test is run (we just test that the input/output are correctly forwarded, and
// hence if the reading test succeeds/fails the other will also succeed/fail)
final SftpProviderTestCase streamProxyTestCase = new SftpProviderTestCase(true);
final ProviderTestSuite sftpStreamSuite = new BaseProviderTestSuite(streamProxyTestCase)
{
@Override
protected void addBaseTests() throws Exception
{
// Just tries to read
addTests(ProviderReadTests.class);
}
};
suite.addTest(sftpStreamSuite);
// Decorate the test suite to set up the Sftp server
final TestSetup setup = new TestSetup(suite)
{
@Override
protected void setUp() throws Exception
{
if (getSystemTestUriOverride() == null)
{
setUpClass();
}
super.setUp();
}
@Override
protected void tearDown() throws Exception
{
// Close SFTP server if needed
tearDownClass();
super.tearDown();
}
};
return setup;
}
/**
* Stops the embedded Apache SSHd Server (MINA).
*
* @throws InterruptedException
*/
private static void tearDownClass() throws InterruptedException
{
if (Server != null)
{
Server.stop();
}
}
public SftpProviderTestCase(final boolean streamProxyMode) throws IOException
{
this.streamProxyMode = streamProxyMode;
}
/**
* 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());
if (streamProxyMode)
{
final FileSystemOptions proxyOptions = (FileSystemOptions) fileSystemOptions.clone();
final URI parsedURI = new URI(uri);
final String userInfo = parsedURI.getUserInfo();
final String[] userFields = userInfo.split(":", 2);
builder.setProxyType(fileSystemOptions, SftpFileSystemConfigBuilder.PROXY_STREAM);
builder.setProxyUser(fileSystemOptions, userFields[0]);
if (userFields.length > 1)
{
builder.setProxyPassword(fileSystemOptions, userFields[1]);
}
builder.setProxyHost(fileSystemOptions, parsedURI.getHost());
builder.setProxyPort(fileSystemOptions, parsedURI.getPort());
builder.setProxyCommand(fileSystemOptions, SftpStreamProxy.NETCAT_COMMAND);
builder.setProxyOptions(fileSystemOptions, proxyOptions);
builder.setProxyPassword(fileSystemOptions, parsedURI.getAuthority());
// Set up the new URI
uri = String.format("sftp://%s@localhost:%d", userInfo, parsedURI.getPort());
}
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+)");
@Override
public Command createCommand(final String command)
{
return new Command()
{
public ExitCallback callback = null;
public OutputStream out = null;
public OutputStream err = null;
public InputStream in = null;
@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"))
{
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()
{
}
};
}
}
/**
* 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(new Runnable()
{
@Override
public void run()
{
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 ex)
{
// Nothing to do, this occurs when the connection
// is closed on the remote side
}
catch (final IOException ex)
{
if (!ex.getMessage().equals("Pipe closed"))
{
code = -1;
}
}
if (callback != null)
{
callback.onExit(code);
}
}
}, name);
thread.setDaemon(true);
thread.start();
}
private static class SftpAttrs
{
int flags = 0;
private int uid;
long size = 0;
private int gid;
private int atime;
private int permissions;
private int mtime;
private String[] extended;
private SftpAttrs(final Buffer buf)
{
int flags = 0;
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<String, Integer>();
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);
}
}
}
}
}