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 213 // Wait until the thread is initialized 214 synchronized(startLock){ 215 try { 216 // Start here in case server thread runs faster than main thread. 217 // See https://sourceforge.net/tracker/?func=detail&atid=1006533&aid=1925590&group_id=208647 218 serverThread.start(); 219 220 startLock.wait(); 221 } 222 catch (InterruptedException e) { 223 e.printStackTrace(); 224 throw new MockFtpServerException(e); 225 } 226 } 227 } 228 229 /** 230 * The logic for the server thread 231 * @see java.lang.Runnable#run() 232 */ 233 public void run() { 234 try { 235 LOG.info("Starting the server on port " + serverControlPort); 236 serverSocket = serverSocketFactory.createServerSocket(serverControlPort); 237 238 // Notify to allow the start() method to finish and return 239 synchronized(startLock) { 240 startLock.notify(); 241 } 242 243 serverSocket.setSoTimeout(500); 244 while(!terminate) { 245 try { 246 Socket clientSocket = serverSocket.accept(); 247 LOG.info("Connection accepted from host " + clientSocket.getInetAddress()); 248 249 DefaultSession session = new DefaultSession(clientSocket, commandHandlers); 250 Thread sessionThread = new Thread(session); 251 sessionThread.start(); 252 253 SessionInfo sessionInfo = new SessionInfo(); 254 sessionInfo.socket = clientSocket; 255 sessionInfo.thread = sessionThread; 256 sessions.put(session, sessionInfo); 257 } 258 catch(SocketTimeoutException socketTimeoutException) { 259 LOG.trace("Socket accept() timeout"); 260 } 261 } 262 } 263 catch (IOException e) { 264 LOG.error("Error", e); 265 } 266 finally { 267 268 LOG.debug("Cleaning up server..."); 269 270 try { 271 serverSocket.close(); 272 273 for (Iterator iter = sessions.keySet().iterator(); iter.hasNext();) { 274 Session session = (Session) iter.next(); 275 SessionInfo sessionInfo = (SessionInfo) sessions.get(session); 276 session.close(); 277 sessionInfo.thread.join(500L); 278 Socket sessionSocket = (Socket) sessionInfo.socket; 279 if (sessionSocket != null) { 280 sessionSocket.close(); 281 } 282 } 283 } 284 catch (IOException e) { 285 e.printStackTrace(); 286 throw new MockFtpServerException(e); 287 } 288 catch (InterruptedException e) { 289 e.printStackTrace(); 290 throw new MockFtpServerException(e); 291 } 292 LOG.info("Server stopped."); 293 } 294 } 295 296 /** 297 * Stop this server instance and wait for it to terminate. 298 */ 299 public void stop() { 300 301 LOG.trace("Stopping the server..."); 302 terminate = true; 303 304 try { 305 serverThread.join(); 306 } 307 catch (InterruptedException e) { 308 e.printStackTrace(); 309 throw new MockFtpServerException(e); 310 } 311 } 312 313 /** 314 * Return the CommandHandler defined for the specified command name 315 * @param name - the command name 316 * @return the CommandHandler defined for name 317 */ 318 public CommandHandler getCommandHandler(String name) { 319 return (CommandHandler) commandHandlers.get(Command.normalizeName(name)); 320 } 321 322 /** 323 * Override the default CommandHandlers with those in the specified Map of 324 * commandName>>CommandHandler. This will only override the default CommandHandlers 325 * for the keys in <code>commandHandlerMapping</code>. All other default CommandHandler 326 * mappings remain unchanged. 327 * 328 * @param commandHandlers - the Map of commandName->CommandHandler; these override the defaults 329 * 330 * @throws AssertFailedException - if the commandHandlerMapping is null 331 */ 332 public void setCommandHandlers(Map commandHandlerMapping) { 333 Assert.notNull(commandHandlerMapping, "commandHandlers"); 334 for (Iterator iter = commandHandlerMapping.keySet().iterator(); iter.hasNext();) { 335 String commandName = (String) iter.next(); 336 setCommandHandler(commandName, (CommandHandler) commandHandlerMapping.get(commandName)); 337 } 338 } 339 340 /** 341 * Set the CommandHandler for the specified command name. If the CommandHandler implements 342 * the {@link ReplyTextBundleAware} interface and its <code>replyTextBundle</code> attribute 343 * is null, then set its <code>replyTextBundle</code> to the <code>replyTextBundle</code> of 344 * this StubFtpServer. 345 * 346 * @param commandName - the command name to which the CommandHandler will be associated 347 * @param commandHandler - the CommandHandler 348 * 349 * @throws AssertFailedException - if the commandName or commandHandler is null 350 */ 351 public void setCommandHandler(String commandName, CommandHandler commandHandler) { 352 Assert.notNull(commandName, "commandName"); 353 Assert.notNull(commandHandler, "commandHandler"); 354 commandHandlers.put(Command.normalizeName(commandName), commandHandler); 355 ReplyTextBundleUtil.setReplyTextBundleIfAppropriate(commandHandler, replyTextBundle); 356 } 357 358 /** 359 * Set the reply text ResourceBundle to a new ResourceBundle with the specified base name, 360 * accessible on the CLASSPATH. See {@link ResourceBundle#getBundle(String)}. 361 * @param baseName - the base name of the resource bundle, a fully qualified class name 362 */ 363 public void setReplyTextBaseName(String baseName) { 364 replyTextBundle = ResourceBundle.getBundle(baseName); 365 } 366 367 /** 368 * Set the port number to which the server control connection socket will bind. The default value is 21. 369 * @param serverControlPort - the port number for the server control connection ServerSocket 370 */ 371 public void setServerControlPort(int serverControlPort) { 372 this.serverControlPort = serverControlPort; 373 } 374 375 //------------------------------------------------------------------------- 376 // Internal Helper Methods 377 //------------------------------------------------------------------------- 378 379 /** 380 * Return true if this server is fully shutdown -- i.e., there is no active (alive) threads and 381 * all sockets are closed. This method is intended for testing only. 382 * @return true if this server is fully shutdown 383 */ 384 boolean isShutdown() { 385 boolean shutdown = !serverThread.isAlive() && serverSocket.isClosed(); 386 387 for (Iterator iter = sessions.keySet().iterator(); iter.hasNext();) { 388 SessionInfo sessionInfo = (SessionInfo) iter.next(); 389 shutdown = shutdown && sessionInfo.socket.isClosed() && !sessionInfo.thread.isAlive(); 390 } 391 return shutdown; 392 } 393 394 /** 395 * Return true if this server has started -- i.e., there is an active (alive) server threads 396 * and non-null server socket. This method is intended for testing only. 397 * @return true if this server has started 398 */ 399 boolean isStarted() { 400 return serverThread != null && serverThread.isAlive() && serverSocket != null; 401 } 402 403}