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}