All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.rmannibucau.rules.api.sftp.SftpServerRule Maven / Gradle / Ivy

The newest version!
package com.github.rmannibucau.rules.api.sftp;

import com.github.rmannibucau.rules.api.LifecycleUnitException;
import com.github.rmannibucau.rules.api.UnitInject;
import com.github.rmannibucau.rules.internal.Reflections;
import org.apache.sshd.SshServer;
import org.apache.sshd.common.NamedFactory;
import org.apache.sshd.common.Session;
import org.apache.sshd.common.file.FileSystemFactory;
import org.apache.sshd.common.file.FileSystemView;
import org.apache.sshd.common.file.SshFile;
import org.apache.sshd.server.Command;
import org.apache.sshd.server.PasswordAuthenticator;
import org.apache.sshd.server.UserAuth;
import org.apache.sshd.server.auth.UserAuthPassword;
import org.apache.sshd.server.command.ScpCommandFactory;
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
import org.apache.sshd.server.session.ServerSession;
import org.apache.sshd.server.sftp.SftpSubsystem;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.mockftpserver.core.util.IoUtil;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

public class SftpServerRule implements TestRule {
	public static final String CLASSPATH_PREFIX = "classpath:";

	private final Object instance;

	private ThreadLocal data = new ThreadLocal() {
		@Override
		protected Data initialValue() {
			return new Data();
		}
	};

	public SftpServerRule() {
		this(null);
	}

	public SftpServerRule(final Object instance) {
		this.instance = instance;
	}

	@Override
	public Statement apply(final Statement base, final Description description) {
		return new Statement() {
			@Override
			public void evaluate() throws Throwable {
				SftpServer server = description.getAnnotation(SftpServer.class);
				if (server == null) {
					server = description.getTestClass().getAnnotation(SftpServer.class);
				}

				if (server == null) {
					base.evaluate();
					return;
				}

				final Data d = data.get();
				d.user = server.user();
				d.password = server.password();
				d.files = new ConcurrentHashMap();

				for (final SftpFile file : server.value()) {
					final String name = file.name();
					final String content = file.content();
					addFile(name, content);
				}
				d.files.put("/", new FakeSftpFile("/", null, d.user, false, d.files));
				d.files.put(".", new FakeSftpFile(".", null, d.user, false, d.files));

				final SshServer ssh = SshServer.setUpDefaultServer();
				ssh.setPort(server.port());
				ssh.setKeyPairProvider(new SimpleGeneratorHostKeyProvider());
				ssh.setUserAuthFactories(Arrays.> asList(new UserAuthPassword.Factory()));
				ssh.setPasswordAuthenticator(new PasswordAuthenticator() {
					@Override
					public boolean authenticate(final String username, final String password, final ServerSession session) {
						return d.user.equals(username) && password.equals(password);
					}
				});
				ssh.setCommandFactory(new ScpCommandFactory());
				ssh.setSubsystemFactories(Arrays.> asList(new SftpSubsystem.Factory()));
				ssh.setFileSystemFactory(new FileSystemFactory() {
					@Override
					public FileSystemView createFileSystemView(final Session session) throws IOException {
						return new FileSystemView() {
							@Override
							public SshFile getFile(final String file) {
								return d.files.get(file);
							}

							@Override
							public SshFile getFile(final SshFile baseDir, final String file) {
								return getFile(baseDir.getAbsolutePath() + '/' + file);
							}

							@Override
							public FileSystemView getNormalizedView() {
								return this;
							}
						};
					}
				});

				ssh.start();

				System.setProperty(SftpServer.PORT, Integer.toString(ssh.getPort()));
				inject(ssh);

				try {
					base.evaluate();
				}
				finally {
					data.remove();
					ssh.stop();
				}
			}
		};
	}

	public String getUser() {
		return data.get().user;
	}

	public String getPassword() {
		return data.get().password;
	}

	public void addFile(final String name, final String contentOrPath) {
		final Data d = data.get();
		final String key = (!name.startsWith("/") ? "/" : "") + name;
		d.files.put(key, new FakeSftpFile(key, content(contentOrPath), d.user, true, Collections. emptyMap()));
		final String[] path = key.split("/");
		final StringBuilder folder = new StringBuilder();
		for (int i = 0; i < path.length - 1; i++) {
			if (i > 0) {
				folder.append('/');
			}
			folder.append(path[ i ]);

			final String current = folder.toString();
			d.files.putIfAbsent(current, new FakeSftpFile(current, null, d.user, false, d.files));
		}
	}

	private void inject(final SshServer server) throws IllegalAccessException {
		for (final Field f : Reflections.findFields(instance.getClass(), UnitInject.class)) {
			if (SshServer.class.equals(f.getType())) {
				checks(f);
				Reflections.set(f, instance, server);
			}
		}
	}

	private void checks(final Field f) {
		if (Reflections.get(f, instance, null) != null) {
			throw new LifecycleUnitException("Field already set: " + f.getName());
		} else if (instance == null) {
			throw new LifecycleUnitException("Pass test instance as parameter constructor if you want to inject FileSystem or FtpServer");
		}
	}

	private static byte[] content(final String content) {
		if (content.startsWith(CLASSPATH_PREFIX)) {
			try {
				final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
				return IoUtil.readBytes(contextClassLoader.getResourceAsStream(content.substring(CLASSPATH_PREFIX.length())));
			}
			catch (IOException e) {
				throw new LifecycleUnitException(e);
			}
		}
		return content.getBytes();
	}

	private static class FakeSftpFile implements SshFile {
		private final String path;
		private final boolean isRegular;
		private final byte[] entry;
		private final String user;
		private final Map all;
		private Map attributes = new HashMap();
		private long lastModified;

		public FakeSftpFile(final String path, final byte[] content, final String user, final boolean isRegular,
				final Map files) {
			this.path = path;
			this.entry = content;
			this.user = user;
			this.isRegular = isRegular;
			this.lastModified = System.currentTimeMillis();
			this.all = files;

			initAttributes();
		}

		private void initAttributes() {
			this.attributes.put(Attribute.CreationTime, lastModified);
			this.attributes.put(Attribute.IsDirectory, isDirectory());
			this.attributes.put(Attribute.IsRegularFile, isFile());
			this.attributes.put(Attribute.IsSymbolicLink, false);
			this.attributes.put(Attribute.Size, getSize());
			this.attributes.put(Attribute.Permissions, EnumSet.allOf(Permission.class));
			this.attributes.put(Attribute.Owner, getOwner());
			this.attributes.put(Attribute.Group, getOwner());
			this.attributes.put(Attribute.LastModifiedTime, getLastModified());
		}

		@Override
		public String getAbsolutePath() {
			return path;
		}

		@Override
		public String getName() {
			return path;
		}

		@Override
		public Map getAttributes(final boolean followLinks) throws IOException {
			return attributes;
		}

		@Override
		public void setAttributes(final Map attributes) throws IOException {
			this.attributes = attributes;
		}

		@Override
		public Object getAttribute(final Attribute attribute, final boolean followLinks) throws IOException {
			return attributes.get(attribute);
		}

		@Override
		public void setAttribute(final Attribute attribute, final Object value) throws IOException {
			attributes.put(attribute, value);
		}

		@Override
		public String readSymbolicLink() throws IOException {
			return null;
		}

		@Override
		public void createSymbolicLink(final SshFile destination) throws IOException {
			// no-op
		}

		@Override
		public String getOwner() {
			return user;
		}

		@Override
		public boolean isDirectory() {
			return !isRegular;
		}

		@Override
		public boolean isFile() {
			return isRegular;
		}

		@Override
		public boolean doesExist() {
			return true;
		}

		@Override
		public boolean isReadable() {
			return true;
		}

		@Override
		public boolean isWritable() {
			return false;
		}

		@Override
		public boolean isExecutable() {
			return false;
		}

		@Override
		public boolean isRemovable() {
			return false;
		}

		@Override
		public SshFile getParentFile() {
			return null;
		}

		@Override
		public long getLastModified() {
			return lastModified;
		}

		@Override
		public boolean setLastModified(final long time) {
			lastModified = time;
			return true;
		}

		@Override
		public long getSize() {
			return entry != null ? entry.length : -1;
		}

		@Override
		public boolean mkdir() {
			return isDirectory();
		}

		@Override
		public boolean delete() {
			return false;
		}

		@Override
		public boolean create() throws IOException {
			return false;
		}

		@Override
		public void truncate() throws IOException {
			// no-op
		}

		@Override
		public boolean move(final SshFile destination) {
			return false;
		}

		@Override
		public List listSshFiles() {
			final List children = new LinkedList();
			if (!isDirectory()) {
				return children;
			}

			for (final Map.Entry entry : all.entrySet()) {
				final FakeSftpFile value = entry.getValue();
				if (entry.getKey().startsWith(path) && value != this) {
					children.add(value);
				}
			}
			return children;
		}

		@Override
		public OutputStream createOutputStream(final long offset) throws IOException {
			return new ByteArrayOutputStream();
		}

		@Override
		public InputStream createInputStream(final long offset) throws IOException {
			return new ByteArrayInputStream(entry != null ? entry: new byte[0]);
		}

		@Override
		public void handleClose() throws IOException {
			// no-op
		}
	}

	private static class Data {
		private String user;
		private String password;
		private ConcurrentMap files;
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy