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