blob: 64428b798a36e05324aa991a43b10c3b2a821a08 [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;
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.time.Duration;
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.lang3.StringUtils;
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) -> StringUtils.equals(username, 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());
builder.setConnectTimeout(fileSystemOptions, Duration.ofSeconds(60));
builder.setSessionTimeout(fileSystemOptions, Duration.ofSeconds(60));
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);
}
}
}
}
}