/** * Copyright (c) Codice Foundation * <p/> * This is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser * General Public License as published by the Free Software Foundation, either version 3 of the * License, or any later version. * <p/> * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. A copy of the GNU Lesser General Public License * is distributed along with this program and can be found at * <http://www.gnu.org/licenses/lgpl.html>. */ package ddf.catalog.ftp; import java.io.File; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; import org.apache.commons.collections.MapUtils; import org.apache.ftpserver.FtpServer; import org.apache.ftpserver.FtpServerConfigurationException; import org.apache.ftpserver.FtpServerFactory; import org.apache.ftpserver.ftplet.FtpStatistics; import org.apache.ftpserver.ftplet.Ftplet; import org.apache.ftpserver.ftplet.UserManager; import org.apache.ftpserver.impl.DefaultFtpServer; import org.apache.ftpserver.listener.Listener; import org.apache.ftpserver.listener.ListenerFactory; import org.apache.ftpserver.ssl.ClientAuth; import org.apache.ftpserver.ssl.SslConfigurationFactory; import org.codice.ddf.configuration.PropertyResolver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ddf.catalog.ftp.ftplets.FtpRequestHandler; /** * Registers the {@link FtpRequestHandler} and starts the FTP server for the FTP Endpoint. */ public class FtpServerStarter { private static final Logger LOGGER = LoggerFactory.getLogger(FtpServerStarter.class); private static final String DEFAULT_LISTENER = "default"; public static final String PORT = "port"; public static final String CLIENT_AUTH = "clientAuth"; public static final String WANT = "want"; public static final String NEED = "need"; private static int maxSleepTimeMillis = 60000; private static int resetWaitTimeMillis = 5000; private int port; private ClientAuth clientAuth = ClientAuth.WANT; private static FtpServer server; private static FtpServerFactory serverFactory; private static UserManager userManager; private static ListenerFactory listenerFactory; private static SslConfigurationFactory sslConfigurationFactory; private Ftplet ftplet; private File keyStoreFile; private String keyStorePassword; private String keyStoreType; private File trustStoreFile; private String trustStorePassword; private String trustStoreType; public FtpServerStarter(Ftplet ftplet, FtpServerFactory serverFactory, ListenerFactory listenerFactory, UserManager userManager, SslConfigurationFactory sslConfigurationFactory) { notNull(ftplet, "ftplet"); notNull(serverFactory, "serverFactory"); notNull(listenerFactory, "listenerFactory"); notNull(userManager, "userManager"); notNull(sslConfigurationFactory, "sslConfigurationFactory"); this.ftplet = ftplet; this.serverFactory = serverFactory; this.listenerFactory = listenerFactory; this.userManager = userManager; this.sslConfigurationFactory = sslConfigurationFactory; } public void init() { configureSslConfigurationFactory(); configureListenerFactory(); configureServerFactory(); server = serverFactory.createServer(); if (server != null) { startServer(); } } public void destroy() { if (server != null && !isStopped()) { server.stop(); LOGGER.debug("FTP server stopped"); } } /** * Callback for when the FTP Endpoint configuration is updated through the Admin UI * * @param properties map of configurable properties */ public void updateConfiguration(Map<String, Object> properties) { if (MapUtils.isEmpty(properties)) { LOGGER.warn("Received null or empty FTP Endpoint configuration. Check the 'FTP Endpoint' configuration."); return; } LOGGER.debug("Updating FTP Endpoint configuration"); Boolean restart = false; if (properties.get(PORT) instanceof String) { //using PropertyResolver in case properties.get("port") is ${org.codice.ddf.catalog.ftp.port} PropertyResolver propertyResolver = new PropertyResolver((String) properties.get("port")); int port = Integer.parseInt(propertyResolver.getResolvedString()); if (this.port != port) { setPort(port); restart = true; } } if (properties.get(CLIENT_AUTH) instanceof String) { String clientAuth = ((String) properties.get("clientAuth")).toLowerCase(); if (!this.clientAuth.toString() .equalsIgnoreCase(clientAuth)) { setClientAuth(clientAuth); restart = true; } } if (restart) { restartDefaultListener(); } } private void configureListenerFactory() { try { listenerFactory.setSslConfiguration(sslConfigurationFactory.createSslConfiguration()); } catch (FtpServerConfigurationException e) { LOGGER.warn( "Failed to create an SSL configuration, server will not start. Verify keystore and trustore paths and passwords are correct"); throw new FtpServerConfigurationException(); } listenerFactory.setPort(port); } private void restartDefaultListener() { LOGGER.debug("Restarting FTP server with new port {}.", port); if (server != null) { waitForConnections(); suspendServer(); destroyDefaultListener(); configureSslConfigurationFactory(); configureListenerFactory(); addDefaultListener(); startServer(); } } private void configureSslConfigurationFactory() { sslConfigurationFactory.setClientAuthentication(clientAuth.toString()); sslConfigurationFactory.setKeystoreFile(keyStoreFile); sslConfigurationFactory.setKeystorePassword(keyStorePassword); sslConfigurationFactory.setKeystoreType(keyStoreType); sslConfigurationFactory.setTruststoreFile(trustStoreFile); sslConfigurationFactory.setTruststorePassword(trustStorePassword); sslConfigurationFactory.setTruststoreType(trustStoreType); } private void configureServerFactory() { serverFactory.addListener(DEFAULT_LISTENER, listenerFactory.createListener()); Map<String, Ftplet> ftplets = new HashMap<>(); ftplets.put(FtpRequestHandler.class.getName(), ftplet); serverFactory.setFtplets(ftplets); serverFactory.setUserManager(userManager); } private void startServer() { if (isStopped() || isSuspended()) { try { server.start(); LOGGER.debug("FTP server started on port {}", port); } catch (Exception e) { LOGGER.warn("Failed to start FTP server", e); } } } private void destroyDefaultListener() { Listener defaultListener = getDefaultListener(); if (!defaultListener.isStopped()) { defaultListener.stop(); } ((DefaultFtpServer) server).getListeners() .clear(); } private void addDefaultListener() { ((DefaultFtpServer) server).getListeners() .put(DEFAULT_LISTENER, listenerFactory.createListener()); } private void suspendServer() { if (!isSuspended()) { server.suspend(); } } private void waitForConnections() { FtpStatistics serverStatistics = ((DefaultFtpServer) server).getServerContext() .getFtpStatistics(); int totalWait = 0; while (serverStatistics.getCurrentConnectionNumber() > 0) { LOGGER.debug("Waiting for {} connections to close before updating configuration", serverStatistics.getCurrentConnectionNumber()); try { if (totalWait <= maxSleepTimeMillis) { totalWait += resetWaitTimeMillis; Thread.sleep(resetWaitTimeMillis); } else { LOGGER.debug( "Waited {} seconds for connections to close, updating FTP configuration", TimeUnit.MILLISECONDS.toSeconds(totalWait)); break; } } catch (InterruptedException e) { Thread.interrupted(); LOGGER.info("Thread interrupted while waiting for FTP connections to close", e); } } } private boolean isStopped() { return server.isStopped(); } private boolean isSuspended() { return server.isSuspended(); } private Listener getDefaultListener() { return ((DefaultFtpServer) server).getListener(DEFAULT_LISTENER); } public int getPort() { return this.port; } public ClientAuth getClientAuthMode() { return clientAuth; } public void setClientAuth(String newClientAuth) { switch (newClientAuth.toLowerCase()) { case WANT: clientAuth = ClientAuth.WANT; break; case NEED: clientAuth = ClientAuth.NEED; break; default: LOGGER.debug("Invalid clientAuth configuration, defaulting to WANT"); clientAuth = ClientAuth.WANT; } } public void setPort(int port) { this.port = port; } public void setKeyStoreFile(String keyStoreFilePath) { keyStoreFile = new File(keyStoreFilePath); } public void setKeyStorePassword(String password) { keyStorePassword = password; } public void setKeyStoreType(String type) { keyStoreType = type; } public void setTrustStoreFile(String trustStoreFilePath) { trustStoreFile = new File(trustStoreFilePath); } public void setTrustStorePassword(String password) { trustStorePassword = password; } public void setTrustStoreType(String type) { trustStoreType = type; } private void notNull(Object object, String name) { if (object == null) { throw new IllegalArgumentException(name + " cannot be null"); } } /** * For testing purposes */ protected void setMaxSleepTime(int seconds) { this.maxSleepTimeMillis = seconds; } /** * For testing purposes */ protected void setResetWaitTime(int seconds) { this.resetWaitTimeMillis = seconds; } }