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 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
153    // Map of Session -> SessionInfo
154    private Map sessions = new HashMap();
155
156    /**
157     * Create a new instance. Initialize the default command handlers and
158     * reply text ResourceBundle.
159     */
160    public StubFtpServer() {
161        replyTextBundle = ResourceBundle.getBundle(REPLY_TEXT_BASENAME);
162
163        commandHandlers = new HashMap();
164
165        PwdCommandHandler pwdCommandHandler = new PwdCommandHandler();
166
167        // Initialize the default CommandHandler mappings
168        setCommandHandler(CommandNames.ABOR, new AborCommandHandler());
169        setCommandHandler(CommandNames.ACCT, new AcctCommandHandler());
170        setCommandHandler(CommandNames.ALLO, new AlloCommandHandler());
171        setCommandHandler(CommandNames.APPE, new AppeCommandHandler());
172        setCommandHandler(CommandNames.PWD, pwdCommandHandler);            // same as XPWD
173        setCommandHandler(CommandNames.CONNECT, new ConnectCommandHandler());
174        setCommandHandler(CommandNames.CWD, new CwdCommandHandler());
175        setCommandHandler(CommandNames.CDUP, new CdupCommandHandler());
176        setCommandHandler(CommandNames.DELE, new DeleCommandHandler());
177        setCommandHandler(CommandNames.HELP, new HelpCommandHandler());
178        setCommandHandler(CommandNames.LIST, new ListCommandHandler());
179        setCommandHandler(CommandNames.MKD, new MkdCommandHandler());
180        setCommandHandler(CommandNames.MODE, new ModeCommandHandler());
181        setCommandHandler(CommandNames.NOOP, new NoopCommandHandler());
182        setCommandHandler(CommandNames.NLST, new NlstCommandHandler());
183        setCommandHandler(CommandNames.PASS, new PassCommandHandler());
184        setCommandHandler(CommandNames.PASV, new PasvCommandHandler());
185        setCommandHandler(CommandNames.PORT, new PortCommandHandler());
186        setCommandHandler(CommandNames.RETR, new RetrCommandHandler());
187        setCommandHandler(CommandNames.QUIT, new QuitCommandHandler());
188        setCommandHandler(CommandNames.REIN, new ReinCommandHandler());
189        setCommandHandler(CommandNames.REST, new RestCommandHandler());
190        setCommandHandler(CommandNames.RMD, new RmdCommandHandler());
191        setCommandHandler(CommandNames.RNFR, new RnfrCommandHandler());
192        setCommandHandler(CommandNames.RNTO, new RntoCommandHandler());
193        setCommandHandler(CommandNames.SITE, new SiteCommandHandler());
194        setCommandHandler(CommandNames.SMNT, new SmntCommandHandler());
195        setCommandHandler(CommandNames.STAT, new StatCommandHandler());
196        setCommandHandler(CommandNames.STOR, new StorCommandHandler());
197        setCommandHandler(CommandNames.STOU, new StouCommandHandler());
198        setCommandHandler(CommandNames.STRU, new StruCommandHandler());
199        setCommandHandler(CommandNames.SYST, new SystCommandHandler());
200        setCommandHandler(CommandNames.TYPE, new TypeCommandHandler());
201        setCommandHandler(CommandNames.USER, new UserCommandHandler());
202        setCommandHandler(CommandNames.XPWD, pwdCommandHandler);           // same as PWD
203    }
204
205    /**
206     * Start a new Thread for this server instance
207     */
208    public void start() {
209        serverThread = new Thread(this);
210        serverThread.start();
211    }
212
213    /**
214     * The logic for the server thread
215     * @see java.lang.Runnable#run()
216     */
217    public void run() {
218        try {
219            LOG.info("Starting the server...");
220            serverSocket = serverSocketFactory.createServerSocket(SERVER_CONTROL_PORT);
221
222            serverSocket.setSoTimeout(500);
223            while(!terminate) {
224                try {
225                    Socket clientSocket = serverSocket.accept();
226                    LOG.info("Connection accepted from host " + clientSocket.getInetAddress());
227
228                    DefaultSession session = new DefaultSession(clientSocket, commandHandlers);
229                    Thread sessionThread = new Thread(session);
230                    sessionThread.start();
231
232                    SessionInfo sessionInfo = new SessionInfo();
233                    sessionInfo.socket = clientSocket;
234                    sessionInfo.thread = sessionThread;
235                    sessions.put(session, sessionInfo);
236                }
237                catch(SocketTimeoutException socketTimeoutException) {
238                    LOG.trace("Socket accept() timeout");
239                }
240            }
241        }
242        catch (IOException e) {
243            LOG.error("Error", e);
244        }
245        finally {
246
247            LOG.debug("Cleaning up server...");
248
249            try {
250                serverSocket.close();
251
252                for (Iterator iter = sessions.keySet().iterator(); iter.hasNext();) {
253                    Session session = (Session) iter.next();
254                    SessionInfo sessionInfo = (SessionInfo) sessions.get(session);
255                    session.close();
256                    sessionInfo.thread.join(500L);
257                    Socket sessionSocket = (Socket) sessionInfo.socket;
258                    if (sessionSocket != null) {
259                        sessionSocket.close();
260                    }
261                }
262            }
263            catch (IOException e) {
264                e.printStackTrace();
265                throw new MockFtpServerException(e);
266            }
267            catch (InterruptedException e) {
268                e.printStackTrace();
269                throw new MockFtpServerException(e);
270            }
271            LOG.info("Server stopped.");
272        }
273    }
274
275    /**
276     * Stop this server instance and wait for it to terminate.
277     */
278    public void stop() {
279
280        LOG.trace("Stopping the server...");
281        terminate = true;
282
283        try {
284            serverThread.join();
285        }
286        catch (InterruptedException e) {
287            e.printStackTrace();
288            throw new MockFtpServerException(e);
289        }
290    }
291
292    /**
293     * Return the CommandHandler defined for the specified command name
294     * @param name - the command name
295     * @return the CommandHandler defined for name
296     */
297    public CommandHandler getCommandHandler(String name) {
298        return (CommandHandler) commandHandlers.get(Command.normalizeName(name));
299    }
300
301    /**
302     * Override the default CommandHandlers with those in the specified Map of
303     * commandName>>CommandHandler. This will only override the default CommandHandlers
304     * for the keys in <code>commandHandlerMapping</code>. All other default CommandHandler
305     * mappings remain unchanged.
306     *
307     * @param commandHandlers - the Map of commandName->CommandHandler; these override the defaults
308     *
309     * @throws AssertFailedException - if the commandHandlerMapping is null
310     */
311    public void setCommandHandlers(Map commandHandlerMapping) {
312        Assert.notNull(commandHandlerMapping, "commandHandlers");
313        for (Iterator iter = commandHandlerMapping.keySet().iterator(); iter.hasNext();) {
314            String commandName = (String) iter.next();
315            setCommandHandler(commandName, (CommandHandler) commandHandlerMapping.get(commandName));
316        }
317    }
318
319    /**
320     * Set the CommandHandler for the specified command name. If the CommandHandler implements
321     * the {@link ReplyTextBundleAware} interface and its <code>replyTextBundle</code> attribute
322     * is null, then set its <code>replyTextBundle</code> to the <code>replyTextBundle</code> of
323     * this StubFtpServer.
324     *
325     * @param commandName - the command name to which the CommandHandler will be associated
326     * @param commandHandler - the CommandHandler
327     *
328     * @throws AssertFailedException - if the commandName or commandHandler is null
329     */
330    public void setCommandHandler(String commandName, CommandHandler commandHandler) {
331        Assert.notNull(commandName, "commandName");
332        Assert.notNull(commandHandler, "commandHandler");
333        commandHandlers.put(Command.normalizeName(commandName), commandHandler);
334        ReplyTextBundleUtil.setReplyTextBundleIfAppropriate(commandHandler, replyTextBundle);
335    }
336
337    /**
338     * Set the reply text ResourceBundle to a new ResourceBundle with the specified base name,
339     * accessible on the CLASSPATH. See {@link ResourceBundle#getBundle(String)}.
340     * @param baseName - the base name of the resource bundle, a fully qualified class name
341     */
342    public void setReplyTextBaseName(String baseName) {
343        replyTextBundle = ResourceBundle.getBundle(baseName);
344    }
345
346    //-------------------------------------------------------------------------
347    // Internal Helper Methods
348    //-------------------------------------------------------------------------
349
350    /**
351     * Return true if this server is fully shutdown -- i.e., there is no active (alive) threads and
352     * all sockets are closed. This method is intended for testing only.
353     * @return true if this server is fully shutdown
354     */
355    boolean isShutdown() {
356        boolean shutdown = !serverThread.isAlive() && serverSocket.isClosed();
357
358        for (Iterator iter = sessions.keySet().iterator(); iter.hasNext();) {
359            SessionInfo sessionInfo = (SessionInfo) iter.next();
360            shutdown = shutdown && sessionInfo.socket.isClosed() && !sessionInfo.thread.isAlive();
361        }
362        return shutdown;
363    }
364
365    /**
366     * Return true if this server has started -- i.e., there is an active (alive) server threads
367     * and non-null server socket. This method is intended for testing only.
368     * @return true if this server has started
369     */
370    boolean isStarted() {
371        return serverThread != null && serverThread.isAlive() && serverSocket != null;
372    }
373
374}