1/*
2 * Copyright 2007 the original author or authors.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package org.mockftpserver.stub;
17
18import java.io.IOException;
19import java.net.ServerSocket;
20import java.net.Socket;
21import java.net.SocketTimeoutException;
22import java.util.HashMap;
23import java.util.Iterator;
24import java.util.Map;
25import java.util.ResourceBundle;
26
27import org.apache.log4j.Logger;
28import org.mockftpserver.core.MockFtpServerException;
29import org.mockftpserver.core.command.Command;
30import org.mockftpserver.core.command.CommandHandler;
31import org.mockftpserver.core.command.CommandNames;
32import org.mockftpserver.core.command.ReplyTextBundleAware;
33import org.mockftpserver.core.command.ReplyTextBundleUtil;
34import org.mockftpserver.core.session.DefaultSession;
35import org.mockftpserver.core.session.Session;
36import org.mockftpserver.core.socket.DefaultServerSocketFactory;
37import org.mockftpserver.core.socket.ServerSocketFactory;
38import org.mockftpserver.core.util.Assert;
39import org.mockftpserver.core.util.AssertFailedException;
40import org.mockftpserver.stub.command.AborCommandHandler;
41import org.mockftpserver.stub.command.AcctCommandHandler;
42import org.mockftpserver.stub.command.AlloCommandHandler;
43import org.mockftpserver.stub.command.AppeCommandHandler;
44import org.mockftpserver.stub.command.CdupCommandHandler;
45import org.mockftpserver.stub.command.ConnectCommandHandler;
46import org.mockftpserver.stub.command.CwdCommandHandler;
47import org.mockftpserver.stub.command.DeleCommandHandler;
48import org.mockftpserver.stub.command.HelpCommandHandler;
49import org.mockftpserver.stub.command.ListCommandHandler;
50import org.mockftpserver.stub.command.MkdCommandHandler;
51import org.mockftpserver.stub.command.ModeCommandHandler;
52import org.mockftpserver.stub.command.NlstCommandHandler;
53import org.mockftpserver.stub.command.NoopCommandHandler;
54import org.mockftpserver.stub.command.PassCommandHandler;
55import org.mockftpserver.stub.command.PasvCommandHandler;
56import org.mockftpserver.stub.command.PortCommandHandler;
57import org.mockftpserver.stub.command.PwdCommandHandler;
58import org.mockftpserver.stub.command.QuitCommandHandler;
59import org.mockftpserver.stub.command.ReinCommandHandler;
60import org.mockftpserver.stub.command.RestCommandHandler;
61import org.mockftpserver.stub.command.RetrCommandHandler;
62import org.mockftpserver.stub.command.RmdCommandHandler;
63import org.mockftpserver.stub.command.RnfrCommandHandler;
64import org.mockftpserver.stub.command.RntoCommandHandler;
65import org.mockftpserver.stub.command.SiteCommandHandler;
66import org.mockftpserver.stub.command.SmntCommandHandler;
67import org.mockftpserver.stub.command.StatCommandHandler;
68import org.mockftpserver.stub.command.StorCommandHandler;
69import org.mockftpserver.stub.command.StouCommandHandler;
70import org.mockftpserver.stub.command.StruCommandHandler;
71import org.mockftpserver.stub.command.SystCommandHandler;
72import org.mockftpserver.stub.command.TypeCommandHandler;
73import org.mockftpserver.stub.command.UserCommandHandler;
74
75/**
76 * <b>StubFtpServer</b> is the top-level class for a "stub" implementation of an FTP Server,
77 * suitable for testing FTP client code or standing in for a live FTP server. It supports
78 * the main FTP commands by defining handlers for each of the corresponding low-level FTP
79 * server commands (e.g. RETR, DELE, LIST). These handlers implement the {@link CommandHandler}
80 * interface.
81 * <p>
82 * <b>StubFtpServer</b> works out of the box with default command handlers that return
83 * success reply codes and empty data (for retrieved files, directory listings, etc.).
84 * The command handler for any command can be easily configured to return custom data
85 * or reply codes. Or it can be replaced with a custom {@link CommandHandler}
86 * implementation. This allows simulation of a complete range of both success and
87 * failure scenarios. The command handlers can also be interrogated to verify command
88 * invocation data such as command parameters and timestamps.
89 * <p>
90 * <b>StubFtpServer</b> can be fully configured programmatically or within a Spring Framework
91 * ({@link http://www.springframework.org/}) or similar container.
92 * <p>
93 * <h4>Starting the StubFtpServer</h4>
94 * Here is how to start the <b>StubFtpServer</b> with the default configuration.
95 * <pre><code>
96 * StubFtpServer stubFtpServer = new StubFtpServer();
97 * stubFtpServer.start();
98 * </code></pre>
99 * <p>
100 * <h4>Retrieving Command Handlers</h4>
101 * You can retrieve the existing {@link CommandHandler} defined for an FTP server command
102 * by calling the {@link #getCommandHandler(String)} method, passing in the FTP server
103 * command name. For example:
104 * <pre><code>
105 * PwdCommandHandler pwdCommandHandler = (PwdCommandHandler) stubFtpServer.getCommandHandler("PWD");
106 * </code></pre>
107 * <p>
108 * <h4>Replacing Command Handlers</h4>
109 * You can replace the existing {@link CommandHandler} defined for an FTP server command
110 * by calling the {@link #setCommandHandler(String, CommandHandler)} method, passing
111 * in the FTP server command name and {@link CommandHandler} instance. For example:
112 * <pre><code>
113 * PwdCommandHandler pwdCommandHandler = new PwdCommandHandler();
114 * pwdCommandHandler.setDirectory("some/dir");
115 * stubFtpServer.setCommandHandler("PWD", pwdCommandHandler);
116 * </code></pre>
117 * You can also replace multiple command handlers at once by using the {@link #setCommandHandlers(Map)}
118 * method. That is especially useful when configuring the server through the <b>Spring Framework</b>.
119 * <h4>FTP Command Reply Text ResourceBundle</h4>
120 * <p>
121 * The default text asociated with each FTP command reply code is contained within the
122 * "ReplyText.properties" ResourceBundle file. You can customize these messages by providing a
123 * locale-specific ResourceBundle file on the CLASSPATH, according to the normal lookup rules of
124 * the ResourceBundle class (e.g., "ReplyText_de.properties"). Alternatively, you can
125 * completely replace the ResourceBundle file by calling the calling the
126 * {@link #setReplyTextBaseName(String)} method.
127 *
128 * @version $Revision$ - $Date$
129 *
130 * @author Chris Mair
131 */
132public final class StubFtpServer implements Runnable {
133
134    /** Default basename for reply text ResourceBundle */
135    public static final String REPLY_TEXT_BASENAME = "ReplyText";
136    private static final int DEFAULT_SERVER_CONTROL_PORT = 21;
137
138    private static Logger LOG = Logger.getLogger(StubFtpServer.class);
139
140    // Simple value object that holds the socket and thread for a single session
141    private static class SessionInfo {
142        private Socket socket;
143        private Thread thread;
144    }
145
146    private ServerSocketFactory serverSocketFactory = new DefaultServerSocketFactory();
147    private ServerSocket serverSocket = null;
148    ResourceBundle replyTextBundle;             // non-private for testing only
149    private volatile boolean terminate = false;
150    private Map commandHandlers;
151    private Thread serverThread;
152    private int serverControlPort = DEFAULT_SERVER_CONTROL_PORT;
153    private Object startLock = new Object();
154
155    // Map of Session -> SessionInfo
156    private Map sessions = new HashMap();
157
158    /**
159     * Create a new instance. Initialize the default command handlers and
160     * reply text ResourceBundle.
161     */
162    public StubFtpServer() {
163        replyTextBundle = ResourceBundle.getBundle(REPLY_TEXT_BASENAME);
164
165        commandHandlers = new HashMap();
166
167        PwdCommandHandler pwdCommandHandler = new PwdCommandHandler();
168
169        // Initialize the default CommandHandler mappings
170        setCommandHandler(CommandNames.ABOR, new AborCommandHandler());
171        setCommandHandler(CommandNames.ACCT, new AcctCommandHandler());
172        setCommandHandler(CommandNames.ALLO, new AlloCommandHandler());
173        setCommandHandler(CommandNames.APPE, new AppeCommandHandler());
174        setCommandHandler(CommandNames.PWD, pwdCommandHandler);            // same as XPWD
175        setCommandHandler(CommandNames.CONNECT, new ConnectCommandHandler());
176        setCommandHandler(CommandNames.CWD, new CwdCommandHandler());
177        setCommandHandler(CommandNames.CDUP, new CdupCommandHandler());
178        setCommandHandler(CommandNames.DELE, new DeleCommandHandler());
179        setCommandHandler(CommandNames.HELP, new HelpCommandHandler());
180        setCommandHandler(CommandNames.LIST, new ListCommandHandler());
181        setCommandHandler(CommandNames.MKD, new MkdCommandHandler());
182        setCommandHandler(CommandNames.MODE, new ModeCommandHandler());
183        setCommandHandler(CommandNames.NOOP, new NoopCommandHandler());
184        setCommandHandler(CommandNames.NLST, new NlstCommandHandler());
185        setCommandHandler(CommandNames.PASS, new PassCommandHandler());
186        setCommandHandler(CommandNames.PASV, new PasvCommandHandler());
187        setCommandHandler(CommandNames.PORT, new PortCommandHandler());
188        setCommandHandler(CommandNames.RETR, new RetrCommandHandler());
189        setCommandHandler(CommandNames.QUIT, new QuitCommandHandler());
190        setCommandHandler(CommandNames.REIN, new ReinCommandHandler());
191        setCommandHandler(CommandNames.REST, new RestCommandHandler());
192        setCommandHandler(CommandNames.RMD, new RmdCommandHandler());
193        setCommandHandler(CommandNames.RNFR, new RnfrCommandHandler());
194        setCommandHandler(CommandNames.RNTO, new RntoCommandHandler());
195        setCommandHandler(CommandNames.SITE, new SiteCommandHandler());
196        setCommandHandler(CommandNames.SMNT, new SmntCommandHandler());
197        setCommandHandler(CommandNames.STAT, new StatCommandHandler());
198        setCommandHandler(CommandNames.STOR, new StorCommandHandler());
199        setCommandHandler(CommandNames.STOU, new StouCommandHandler());
200        setCommandHandler(CommandNames.STRU, new StruCommandHandler());
201        setCommandHandler(CommandNames.SYST, new SystCommandHandler());
202        setCommandHandler(CommandNames.TYPE, new TypeCommandHandler());
203        setCommandHandler(CommandNames.USER, new UserCommandHandler());
204        setCommandHandler(CommandNames.XPWD, pwdCommandHandler);           // same as PWD
205    }
206
207    /**
208     * Start a new Thread for this server instance
209     */
210    public void start() {
211        serverThread = new Thread(this);
212        serverThread.start();
213
214        // Wait until the thread is initialized
215        synchronized(startLock){
216            try {
217                startLock.wait();
218            }
219            catch (InterruptedException e) {
220                e.printStackTrace();
221                throw new MockFtpServerException(e);
222            }
223        }
224    }
225
226    /**
227     * The logic for the server thread
228     * @see java.lang.Runnable#run()
229     */
230    public void run() {
231        try {
232            LOG.info("Starting the server on port " + serverControlPort);
233            serverSocket = serverSocketFactory.createServerSocket(serverControlPort);
234
235            // Notify to allow the start() method to finish and return
236            synchronized(startLock) {
237                startLock.notify();
238            }
239
240            serverSocket.setSoTimeout(500);
241            while(!terminate) {
242                try {
243                    Socket clientSocket = serverSocket.accept();
244                    LOG.info("Connection accepted from host " + clientSocket.getInetAddress());
245
246                    DefaultSession session = new DefaultSession(clientSocket, commandHandlers);
247                    Thread sessionThread = new Thread(session);
248                    sessionThread.start();
249
250                    SessionInfo sessionInfo = new SessionInfo();
251                    sessionInfo.socket = clientSocket;
252                    sessionInfo.thread = sessionThread;
253                    sessions.put(session, sessionInfo);
254                }
255                catch(SocketTimeoutException socketTimeoutException) {
256                    LOG.trace("Socket accept() timeout");
257                }
258            }
259        }
260        catch (IOException e) {
261            LOG.error("Error", e);
262        }
263        finally {
264
265            LOG.debug("Cleaning up server...");
266
267            try {
268                serverSocket.close();
269
270                for (Iterator iter = sessions.keySet().iterator(); iter.hasNext();) {
271                    Session session = (Session) iter.next();
272                    SessionInfo sessionInfo = (SessionInfo) sessions.get(session);
273                    session.close();
274                    sessionInfo.thread.join(500L);
275                    Socket sessionSocket = (Socket) sessionInfo.socket;
276                    if (sessionSocket != null) {
277                        sessionSocket.close();
278                    }
279                }
280            }
281            catch (IOException e) {
282                e.printStackTrace();
283                throw new MockFtpServerException(e);
284            }
285            catch (InterruptedException e) {
286                e.printStackTrace();
287                throw new MockFtpServerException(e);
288            }
289            LOG.info("Server stopped.");
290        }
291    }
292
293    /**
294     * Stop this server instance and wait for it to terminate.
295     */
296    public void stop() {
297
298        LOG.trace("Stopping the server...");
299        terminate = true;
300
301        try {
302            serverThread.join();
303        }
304        catch (InterruptedException e) {
305            e.printStackTrace();
306            throw new MockFtpServerException(e);
307        }
308    }
309
310    /**
311     * Return the CommandHandler defined for the specified command name
312     * @param name - the command name
313     * @return the CommandHandler defined for name
314     */
315    public CommandHandler getCommandHandler(String name) {
316        return (CommandHandler) commandHandlers.get(Command.normalizeName(name));
317    }
318
319    /**
320     * Override the default CommandHandlers with those in the specified Map of
321     * commandName>>CommandHandler. This will only override the default CommandHandlers
322     * for the keys in <code>commandHandlerMapping</code>. All other default CommandHandler
323     * mappings remain unchanged.
324     *
325     * @param commandHandlers - the Map of commandName->CommandHandler; these override the defaults
326     *
327     * @throws AssertFailedException - if the commandHandlerMapping is null
328     */
329    public void setCommandHandlers(Map commandHandlerMapping) {
330        Assert.notNull(commandHandlerMapping, "commandHandlers");
331        for (Iterator iter = commandHandlerMapping.keySet().iterator(); iter.hasNext();) {
332            String commandName = (String) iter.next();
333            setCommandHandler(commandName, (CommandHandler) commandHandlerMapping.get(commandName));
334        }
335    }
336
337    /**
338     * Set the CommandHandler for the specified command name. If the CommandHandler implements
339     * the {@link ReplyTextBundleAware} interface and its <code>replyTextBundle</code> attribute
340     * is null, then set its <code>replyTextBundle</code> to the <code>replyTextBundle</code> of
341     * this StubFtpServer.
342     *
343     * @param commandName - the command name to which the CommandHandler will be associated
344     * @param commandHandler - the CommandHandler
345     *
346     * @throws AssertFailedException - if the commandName or commandHandler is null
347     */
348    public void setCommandHandler(String commandName, CommandHandler commandHandler) {
349        Assert.notNull(commandName, "commandName");
350        Assert.notNull(commandHandler, "commandHandler");
351        commandHandlers.put(Command.normalizeName(commandName), commandHandler);
352        ReplyTextBundleUtil.setReplyTextBundleIfAppropriate(commandHandler, replyTextBundle);
353    }
354
355    /**
356     * Set the reply text ResourceBundle to a new ResourceBundle with the specified base name,
357     * accessible on the CLASSPATH. See {@link ResourceBundle#getBundle(String)}.
358     * @param baseName - the base name of the resource bundle, a fully qualified class name
359     */
360    public void setReplyTextBaseName(String baseName) {
361        replyTextBundle = ResourceBundle.getBundle(baseName);
362    }
363
364    /**
365     * Set the port number to which the server control connection socket will bind. The default value is 21.
366     * @param serverControlPort - the port number for the server control connection ServerSocket
367     */
368    public void setServerControlPort(int serverControlPort) {
369        this.serverControlPort = serverControlPort;
370    }
371
372    //-------------------------------------------------------------------------
373    // Internal Helper Methods
374    //-------------------------------------------------------------------------
375
376    /**
377     * Return true if this server is fully shutdown -- i.e., there is no active (alive) threads and
378     * all sockets are closed. This method is intended for testing only.
379     * @return true if this server is fully shutdown
380     */
381    boolean isShutdown() {
382        boolean shutdown = !serverThread.isAlive() && serverSocket.isClosed();
383
384        for (Iterator iter = sessions.keySet().iterator(); iter.hasNext();) {
385            SessionInfo sessionInfo = (SessionInfo) iter.next();
386            shutdown = shutdown && sessionInfo.socket.isClosed() && !sessionInfo.thread.isAlive();
387        }
388        return shutdown;
389    }
390
391    /**
392     * Return true if this server has started -- i.e., there is an active (alive) server threads
393     * and non-null server socket. This method is intended for testing only.
394     * @return true if this server has started
395     */
396    boolean isStarted() {
397        return serverThread != null && serverThread.isAlive() && serverSocket != null;
398    }
399
400}