RepositoryFtpServer.java

/*
 * Copyright 2018 Global Crop Diversity Trust
 *
 * Licensed 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.genesys.filerepository.service.ftp;

import java.io.File;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Map;

import org.apache.ftpserver.ConnectionConfig;
import org.apache.ftpserver.DataConnectionConfigurationFactory;
import org.apache.ftpserver.FtpServer;
import org.apache.ftpserver.FtpServerFactory;
import org.apache.ftpserver.ftplet.DefaultFtplet;
import org.apache.ftpserver.ftplet.FileSystemFactory;
import org.apache.ftpserver.ftplet.FtpException;
import org.apache.ftpserver.ftplet.Ftplet;
import org.apache.ftpserver.ftplet.UserManager;
import org.apache.ftpserver.listener.ListenerFactory;
import org.apache.ftpserver.message.MessageResource;
import org.apache.ftpserver.ssl.SslConfigurationFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

/**
 * The Class RepositoryFtpServer.
 */
@Component
@Slf4j
public class RepositoryFtpServer implements InitializingBean, DisposableBean {

	/** The user manager. */
	@Autowired
	private UserManager userManager;

	/** The message resource. */
	@Autowired(required = false)
	private MessageResource messageResource;

	/** The ftp port. */
	private int ftpPort;

	/** The max threads. */
	private int maxThreads = 20;

	/** The max logins. */
	// The maximum number of simultaneous users
	private int maxLogins = 10;

	/** The idle timeout. */
	// Idle timeout
	private int idleTimeout = 60;

	/** The keystore path. */
	private String keystorePath;

	/** The keystore psw. */
	private String keystorePsw;

	/** The passive ports. */
	private String passivePorts;

	/** The external address. */
	private String externalAddress;

	/** The server. */
	private FtpServer server = null;

	/** The repository file system factory. */
	@Autowired
	private FileSystemFactory repositoryFileSystemFactory;

	/**
	 * Set external address of the FTP server that will be returned to clients on
	 * the PASV command.
	 *
	 * @param externalAddress the new external address
	 * @see DataConnectionConfigurationFactory#setPassiveExternalAddress(String)
	 */
	public void setExternalAddress(final String externalAddress) {
		this.externalAddress = externalAddress;
	}

	/**
	 * Set the passive ports to be used for data connections.
	 *
	 * @param passivePorts the new passive ports
	 * @see DataConnectionConfigurationFactory#setPassivePorts(String)
	 */
	public void setPassivePorts(final String passivePorts) {
		this.passivePorts = passivePorts;
	}

	/**
	 * Set the path to Java keystore
	 *
	 * <pre>
	 * keytool -genkey -alias testdomain -keyalg RSA -keystore ftpserver.jks -keysize 4096
	 * </pre>
	 *
	 * @param keystorePath the new keystore path
	 */
	public void setKeystorePath(final String keystorePath) {
		this.keystorePath = keystorePath;
	}

	/**
	 * Sets the keystore psw.
	 *
	 * @param keystorePsw the new keystore psw
	 */
	public void setKeystorePsw(final String keystorePsw) {
		this.keystorePsw = keystorePsw;
	}

	/**
	 * Sets the user manager.
	 *
	 * @param userManager the new user manager
	 */
	public void setUserManager(final UserManager userManager) {
		this.userManager = userManager;
	}

	/**
	 * Sets the message resource.
	 *
	 * @param messageResource the new message resource
	 */
	public void setMessageResource(final MessageResource messageResource) {
		this.messageResource = messageResource;
	}

	/**
	 * Sets the ftp port.
	 *
	 * @param ftpPort the new ftp port
	 */
	public void setFtpPort(final int ftpPort) {
		this.ftpPort = ftpPort;
	}

	/**
	 * Sets the max logins.
	 *
	 * @param maxLogins the new max logins
	 */
	public void setMaxLogins(final int maxLogins) {
		this.maxLogins = maxLogins;
	}

	/**
	 * Sets the idle timeout.
	 *
	 * @param idleTimeout the new idle timeout
	 */
	public void setIdleTimeout(final int idleTimeout) {
		this.idleTimeout = idleTimeout;
	}

	/**
	 * Sets the max threads.
	 *
	 * @param maxThreads the new max threads
	 */
	public void setMaxThreads(final int maxThreads) {
		this.maxThreads = maxThreads;
	}

	/*
	 * (non-Javadoc)
	 * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
	 */
	@Override
	public void afterPropertiesSet() throws FtpException, UnknownHostException {
		if (this.ftpPort < 1) {
			log.warn("FTP server not started, port={}", ftpPort);
			return;
		}

		final FtpServerFactory serverFactory = new FtpServerFactory();

		serverFactory.setUserManager(userManager);
		if (messageResource != null) {
			serverFactory.setMessageResource(messageResource);
		}

		{
			final ListenerFactory factory = new ListenerFactory();
			// set the port of the listener
			factory.setPort(ftpPort);
			// set idle timeout
			factory.setIdleTimeout(idleTimeout);

			// define SSL configuration
			final SslConfigurationFactory ssl = new SslConfigurationFactory();
			// Create store: keytool -genkey -alias testdomain -keyalg RSA -keystore
			// ftpserver.jks -keysize 4096
			ssl.setKeystoreFile(new File(keystorePath));
			ssl.setKeystorePassword(keystorePsw);

			// set the SSL configuration for the listener
			factory.setSslConfiguration(ssl.createSslConfiguration());
			factory.setImplicitSsl(true);

			// define Data Connection configuration
			final DataConnectionConfigurationFactory dccf = new DataConnectionConfigurationFactory();
			dccf.setPassivePorts(passivePorts);
			if (externalAddress != null) {
				final InetAddress address = InetAddress.getByName(externalAddress);
				dccf.setPassiveExternalAddress(address.getHostAddress());
			}
			factory.setDataConnectionConfiguration(dccf.createDataConnectionConfiguration());

			// replace the default listener
			serverFactory.addListener("default", factory.createListener());
		}

		final FileSystemFactory fileSystem = repositoryFileSystemFactory;
		serverFactory.setFileSystem(fileSystem);

		final ConnectionConfig ftpConnectionConfig = new ConnectionConfig() {
			@Override
			public boolean isAnonymousLoginEnabled() {
				return false;
			}

			@Override
			public int getMaxThreads() {
				return maxThreads;
			}

			@Override
			public int getMaxLogins() {
				return maxLogins;
			}

			// The number of failed login attempts before the connection is closed
			@Override
			public int getMaxLoginFailures() {
				return 3;
			}

			@Override
			public int getMaxAnonymousLogins() {
				return 0;
			}

			// The number of milliseconds that the connection is delayed after a failed
			// login attempt.
			@Override
			public int getLoginFailureDelay() {
				return 30;
			}
		};
		serverFactory.setConnectionConfig(ftpConnectionConfig);

		final Map<String, Ftplet> ftplets = new HashMap<>();
		ftplets.put("default", repositoryFtplet());
		serverFactory.setFtplets(ftplets);

		this.server = serverFactory.createServer();

		log.info("Starting FTP server on port {}", this.ftpPort);
		server.start();
	}

	/*
	 * (non-Javadoc)
	 * @see org.springframework.beans.factory.DisposableBean#destroy()
	 */
	@Override
	public void destroy() throws Exception {
		if (this.server != null) {
			log.info("Shutting down FTP server on port {}", this.ftpPort);
			this.server.stop();
		}
	}

	/**
	 * Repository ftplet.
	 *
	 * @return the ftplet
	 */
	private Ftplet repositoryFtplet() {
		return new DefaultFtplet() {

		};
	}

}