accueilLogicielsDéveloppement et Qualité LogicielsÉcosystème Java
 

Embarquer un serveur FTP(S) en Java

Utilisation d’Apache FTPServer pour embarquer un serveur FTP/FTPS dans un programme Java.


Introduction

Il peut être utile de pouvoir embarquer un serveur FTP ou FTPS en Java pour pouvoir proposer ses fonctionnalités au travers de l’application. Une autre utilisation est de simuler un server FTP(S) pour la réalisation de tests unitaires. La plupart du temps, l’utilisation de mock peut suffire, mais il est parfois nécessaire d’avoir un vrai serveur qui répond.

On peut utiliser pour cela le projet Apache FTPServer (un sous projet de Apache Mina). Ce projet propose un serveur FTP 100% Java pouvant tourner seul ou embarqué.

Cet article montre son utilisation dans le cas de tests unitaires. La totalité du code est à la fin de cet article.

Apache FTPServer

Dépendances

Les dépendances utilisées pour ces tests sont (en plus de JUnit) : Apache FTPServer, ainsi que Apache Commons Net (pour le client FTP). Avec Maven, il suffit de déclarer les dépendances suivantes :

<dependency>
        <groupId>org.apache.ftpserver</groupId>
        <artifactId>ftpserver-core</artifactId>
        <version>1.0.6</version>
</dependency>
<dependency>
        <groupId>commons-net</groupId>
        <artifactId>commons-net</artifactId>
        <version>2.2</version>
</dependency>

Utilisation

Il faut tout d’abord définir un port d’écoute :

final ListenerFactory listenerFactory = new ListenerFactory();
listenerFactory.setPort(port);

Puis associer ce listener à la factory par défaut :

final FtpServerFactory ftpServerFactory = new FtpServerFactory();
ftpServerFactory.addListener("default", listenerFactory.createListener());

Il faut également indiquer comment gérer l’authentification (cela utilise la classe MyUserManager que l’on verra ensuite) :

ftpServerFactory.setUserManager(new MyUserManager());

Enfin, on peut créer le serveur et le démarrer :

final FTPServer ftpServer = ftpServerFactory.createServer();
ftpServer.start();
// Tests à réaliser
ftpServer.stop();

L’authentification se fait en fournissant une classe implémentant l’interface UserManager. Les méthodes les plus importantes sont :

public User getUserByName(final String userName)
// Retourne un objet User à partir du login. Cet objet User
// donne également les informations sur le répertoire HOME,
// les droits d'écriture, les contraintes sur le nombre de logins,
// les débits autorisés, etc.


public User authenticate(final Authentication authentication) throws AuthenticationFailedException
// A partir de l'authentification (par login/mot de passe : UsernamePasswordAuthentication, ou alors par anonyme : AnonymousAuthentication),
// vérifier que l'utilisateur a le droit de se connecter, et si oui,
// retouner un objet User (à l'aide de la méthode getUserByName précédente).

On peut encapsuler le code de création du serveur FTP afin de le réutiliser lors des tests.

Un exemple de test simple, utilisant Apache Commons Net comme client FTP :

@Test
public void goodFTPAuthentication() throws FtpException, IOException {
        final FtpServer ftpServer = createFtpServer(FTP_PORT, false);
        ftpServer.start();

        final FTPClient ftpClient = new FTPClient();
        ftpClient.connect("localhost", FTP_PORT);
        final boolean login = ftpClient.login("bob", "bob");
        assertTrue("Login must be true", login);
        assertEquals("Reply code must be 230", 230, ftpClient.getReplyCode());

        ftpClient.logout();
        ftpServer.stop();
}

Serveur FTPS

Il est également possible de créer un serveur sécuriser FTPS. Il faut d’abord créer un trousseau de clé pour le serveur, ce qui peut être fait de la manière suivante :

keytool -genkey -alias ftptest -keyalg RSA -keystore ftpserver.jks -keysize 4096

Ensuite, il suffit de créer une configuration SSL et de la lier au listener :

final SslConfigurationFactory sslConfigurationFactory = new SslConfigurationFactory();
sslConfigurationFactory.setKeystoreFile(new File(FTPSERVER_ROOT, "ftpserver.jks"));
sslConfigurationFactory.setKeystorePassword("supermdp");
listenerFactory.setSslConfiguration(sslConfigurationFactory.createSslConfiguration());
listenerFactory.setImplicitSsl(false);

Code utilisé dans l’article

import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPSClient;
import org.apache.ftpserver.FtpServer;
import org.apache.ftpserver.FtpServerFactory;
import org.apache.ftpserver.ftplet.*;
import org.apache.ftpserver.listener.ListenerFactory;
import org.apache.ftpserver.ssl.SslConfigurationFactory;
import org.apache.ftpserver.usermanager.AnonymousAuthentication;
import org.apache.ftpserver.usermanager.UsernamePasswordAuthentication;
import org.apache.ftpserver.usermanager.impl.BaseUser;
import org.apache.ftpserver.usermanager.impl.ConcurrentLoginPermission;
import org.apache.ftpserver.usermanager.impl.TransferRatePermission;
import org.apache.ftpserver.usermanager.impl.WritePermission;
import org.junit.Test;

import java.io.File;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import static junit.framework.Assert.*;

public class EmbeddedFtpServerTest {
        private final String FTPSERVER_ROOT = "src/test/resources";
        private final int FTP_PORT = 2221;

        @Test
        public void goodFTPAuthentication() throws FtpException, IOException {
                final FtpServer ftpServer = createFtpServer(FTP_PORT, false);
                ftpServer.start();

                final FTPClient ftpClient = new FTPClient();
                ftpClient.connect("localhost", FTP_PORT);
                final boolean login = ftpClient.login("bob", "bob");
                assertTrue("Login must be true", login);
                assertEquals("Reply code must be 230", 230, ftpClient.getReplyCode());

                ftpClient.logout();
                ftpServer.stop();
        }

        @Test
        public void badFTPAuthentication() throws FtpException, IOException {
                final FtpServer ftpServer = createFtpServer(FTP_PORT, false);
                ftpServer.start();

                final FTPClient ftpClient = new FTPClient();
                ftpClient.connect("localhost", FTP_PORT);
                try {
                        final boolean login = ftpClient.login("bob", "bobby");
                        assertFalse("Login must be false", login);
                } finally {
                        ftpServer.stop();
                }
        }

        @Test
        public void goodFTPSAuthentication() throws FtpException, IOException, NoSuchAlgorithmException {
                final FtpServer ftpServer = createFtpServer(FTP_PORT, true);
                ftpServer.start();

                final FTPSClient ftpsClient = new FTPSClient(false); // implicit = false
                ftpsClient.connect("localhost", FTP_PORT);
                final boolean login = ftpsClient.login("bob", "bob");
                assertTrue("Login must be true", login);
                assertEquals("Reply code must be 230", 230, ftpsClient.getReplyCode());

                ftpsClient.logout();
                ftpServer.stop();
        }

        @Test
        public void listFTPSFiles() throws FtpException, IOException, NoSuchAlgorithmException {
                final FtpServer ftpServer = createFtpServer(FTP_PORT, true);
                ftpServer.start();

                final FTPSClient ftpsClient = new FTPSClient(false); // implicit = false
                ftpsClient.connect("localhost", FTP_PORT);
                final boolean login = ftpsClient.login("bob", "bob");
                assertTrue("Login must be true", login);
                assertEquals("Reply code must be 230", 230, ftpsClient.getReplyCode());

                final String[] filenames = ftpsClient.listNames(FTPSERVER_ROOT);
                assertNotNull(filenames);
                assertTrue("At least one file", filenames.length > 0);

                final boolean containsFile = Arrays.asList(filenames).contains("ftpserver.jks");
                assertTrue("Must list the ftpserver.jks file", containsFile);

                ftpsClient.logout();
                ftpServer.stop();
        }

        private FtpServer createFtpServer(int port, boolean ftps) {
                final ListenerFactory listenerFactory = new ListenerFactory();
                listenerFactory.setPort(port);

                if (ftps) {
                        // SSL config
                        final SslConfigurationFactory sslConfigurationFactory = new SslConfigurationFactory();

                        // Create store: keytool -genkey -alias ftptest -keyalg RSA -keystore ftpserver.jks -keysize 4096
                        sslConfigurationFactory.setKeystoreFile(new File(FTPSERVER_ROOT, "ftpserver.jks"));
                        sslConfigurationFactory.setKeystorePassword("supermdp");
                        listenerFactory.setSslConfiguration(sslConfigurationFactory.createSslConfiguration());
                        listenerFactory.setImplicitSsl(false);
                }

                // Listener
                final FtpServerFactory ftpServerFactory = new FtpServerFactory();
                ftpServerFactory.addListener("default", listenerFactory.createListener());

                // Authentication
                ftpServerFactory.setUserManager(new MyUserManager());

                return ftpServerFactory.createServer();
        }

        class MyUserManager implements UserManager {

                @Override
                public User getUserByName(final String userName) {
                        BaseUser user = new BaseUser();
                        user.setName(userName);
                        user.setEnabled(true);

                        // Home dir = .
                        user.setHomeDirectory("./");

                        List<Authority> authorities = new ArrayList<Authority>();
                        authorities.add(new WritePermission());

                        // No special limit
                        int maxLogin = 0;
                        int maxLoginPerIP = 0;
                        authorities.add(new ConcurrentLoginPermission(maxLogin, maxLoginPerIP));

                        int uploadRate = 0;
                        int downloadRate = 0;
                        authorities.add(new TransferRatePermission(downloadRate, uploadRate));

                        user.setAuthorities(authorities);
                        user.setMaxIdleTime(0);

                        return user;
                }

                @Override
                public String[] getAllUserNames() throws FtpException {
                        return new String[]{"bob"};
                }

                @Override
                public void delete(final String s) throws FtpException {
                }

                @Override
                public void save(final User user) throws FtpException {
                }

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

                @Override
                public User authenticate(final Authentication authentication) throws AuthenticationFailedException {
                        if (authentication instanceof UsernamePasswordAuthentication) {
                                UsernamePasswordAuthentication upauth = (UsernamePasswordAuthentication) authentication;

                                String user = upauth.getUsername();
                                String password = upauth.getPassword();

                                if (user == null) {
                                        throw new AuthenticationFailedException("Authentication failed");
                                }

                                // Simple auth for tests: password = login
                                if (!user.equals(password)) {
                                        throw new AuthenticationFailedException("Dummy authentication: password must be equals to login");
                                }

                                return getUserByName(user);

                        } else if (authentication instanceof AnonymousAuthentication) {
                                if (doesExist("anonymous")) {
                                        return getUserByName("anonymous");
                                } else {
                                        throw new AuthenticationFailedException("Authentication failed");
                                }
                        } else {
                                throw new IllegalArgumentException(
                                        "Authentication not supported by this user manager");
                        }
                }

                @Override
                public String getAdminName() throws FtpException {
                        return "admin";
                }

                @Override
                public boolean isAdmin(final String s) throws FtpException {
                        return "admin".equals(s);
                }
        }

}

 

Stéphane DERACO
Envoyer un courriel

 


ARESU
Direction des Systèmes d'Information du CNRS

358 rue P.-G. de Gennes
31676 LABEGE Cedex

Bâtiment 1,
1 Place Aristide Briand
92195 MEUDON Cedex



 

 

Direction des Systèmes d'Information

Pôle ARESU

Accueil Imprimer Plan du site Credits