1 /* 2 * Copyright 2008 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 */ 16 package org.mockftpserver.fake.command; 17 18 import org.mockftpserver.core.CommandSyntaxException; 19 import org.mockftpserver.core.IllegalStateException; 20 import org.mockftpserver.core.NotLoggedInException; 21 import org.mockftpserver.core.command.AbstractCommandHandler; 22 import org.mockftpserver.core.command.Command; 23 import org.mockftpserver.core.command.ReplyCodes; 24 import org.mockftpserver.core.session.Session; 25 import org.mockftpserver.core.session.SessionKeys; 26 import org.mockftpserver.core.util.Assert; 27 import org.mockftpserver.fake.ServerConfiguration; 28 import org.mockftpserver.fake.ServerConfigurationAware; 29 import org.mockftpserver.fake.UserAccount; 30 import org.mockftpserver.fake.filesystem.FileSystem; 31 import org.mockftpserver.fake.filesystem.FileSystemEntry; 32 import org.mockftpserver.fake.filesystem.FileSystemException; 33 import org.mockftpserver.fake.filesystem.InvalidFilenameException; 34 35 import java.text.MessageFormat; 36 import java.util.ArrayList; 37 import java.util.Collections; 38 import java.util.List; 39 import java.util.MissingResourceException; 40 41 /** 42 * Abstract superclass for CommandHandler classes for the "Fake" server. 43 * 44 * @author Chris Mair 45 * @version $Revision$ - $Date$ 46 */ 47 public abstract class AbstractFakeCommandHandler extends AbstractCommandHandler implements ServerConfigurationAware { 48 49 protected static final String INTERNAL_ERROR_KEY = "internalError"; 50 51 private ServerConfiguration serverConfiguration; 52 53 /** 54 * Reply code sent back when a FileSystemException is caught by the {@link #handleCommand(Command, Session)} 55 * This defaults to ReplyCodes.EXISTING_FILE_ERROR (550). 56 */ 57 protected int replyCodeForFileSystemException = ReplyCodes.READ_FILE_ERROR; 58 59 public ServerConfiguration getServerConfiguration() { 60 return serverConfiguration; 61 } 62 63 public void setServerConfiguration(ServerConfiguration serverConfiguration) { 64 this.serverConfiguration = serverConfiguration; 65 } 66 67 /** 68 * Use template method to centralize and ensure common validation 69 */ 70 public void handleCommand(Command command, Session session) { 71 Assert.notNull(serverConfiguration, "serverConfiguration"); 72 Assert.notNull(command, "command"); 73 Assert.notNull(session, "session"); 74 75 try { 76 handle(command, session); 77 } 78 catch (CommandSyntaxException e) { 79 handleException(command, session, e, ReplyCodes.COMMAND_SYNTAX_ERROR); 80 } 81 catch (IllegalStateException e) { 82 handleException(command, session, e, ReplyCodes.ILLEGAL_STATE); 83 } 84 catch (NotLoggedInException e) { 85 handleException(command, session, e, ReplyCodes.NOT_LOGGED_IN); 86 } 87 catch (InvalidFilenameException e) { 88 handleFileSystemException(command, session, e, ReplyCodes.FILENAME_NOT_VALID, e.getPath()); 89 } 90 catch (FileSystemException e) { 91 handleFileSystemException(command, session, e, replyCodeForFileSystemException, e.getPath()); 92 } 93 } 94 95 /** 96 * Convenience method to return the FileSystem stored in the ServerConfiguration 97 * 98 * @return the FileSystem 99 */ 100 protected FileSystem getFileSystem() { 101 return serverConfiguration.getFileSystem(); 102 } 103 104 /** 105 * Handle the specified command for the session. All checked exceptions are expected to be wrapped or handled 106 * by the caller. 107 * 108 * @param command - the Command to be handled 109 * @param session - the session on which the Command was submitted 110 */ 111 protected abstract void handle(Command command, Session session); 112 113 // ------------------------------------------------------------------------- 114 // Utility methods for subclasses 115 // ------------------------------------------------------------------------- 116 117 /** 118 * Send a reply for this command on the control connection. 119 * <p/> 120 * The reply code is designated by the <code>replyCode</code> property, and the reply text 121 * is retrieved from the <code>replyText</code> ResourceBundle, using the specified messageKey. 122 * 123 * @param session - the Session 124 * @param replyCode - the reply code 125 * @param messageKey - the resource bundle key for the reply text 126 * @throws AssertionError - if session is null 127 * @see MessageFormat 128 */ 129 protected void sendReply(Session session, int replyCode, String messageKey) { 130 sendReply(session, replyCode, messageKey, Collections.EMPTY_LIST); 131 } 132 133 /** 134 * Send a reply for this command on the control connection. 135 * <p/> 136 * The reply code is designated by the <code>replyCode</code> property, and the reply text 137 * is retrieved from the <code>replyText</code> ResourceBundle, using the specified messageKey. 138 * 139 * @param session - the Session 140 * @param replyCode - the reply code 141 * @param messageKey - the resource bundle key for the reply text 142 * @param args - the optional message arguments; defaults to [] 143 * @throws AssertionError - if session is null 144 * @see MessageFormat 145 */ 146 protected void sendReply(Session session, int replyCode, String messageKey, List args) { 147 Assert.notNull(session, "session"); 148 assertValidReplyCode(replyCode); 149 150 String text = getTextForKey(messageKey); 151 String replyText = (args != null && !args.isEmpty()) ? MessageFormat.format(text, args.toArray()) : text; 152 153 String replyTextToLog = (replyText == null) ? "" : " " + replyText; 154 String argsToLog = (args != null && !args.isEmpty()) ? (" args=" + args) : ""; 155 LOG.info("Sending reply [" + replyCode + replyTextToLog + "]" + argsToLog); 156 session.sendReply(replyCode, replyText); 157 } 158 159 /** 160 * Send a reply for this command on the control connection. 161 * <p/> 162 * The reply code is designated by the <code>replyCode</code> property, and the reply text 163 * is retrieved from the <code>replyText</code> ResourceBundle, using the reply code as the key. 164 * 165 * @param session - the Session 166 * @param replyCode - the reply code 167 * @throws AssertionError - if session is null 168 * @see MessageFormat 169 */ 170 protected void sendReply(Session session, int replyCode) { 171 sendReply(session, replyCode, Collections.EMPTY_LIST); 172 } 173 174 /** 175 * Send a reply for this command on the control connection. 176 * <p/> 177 * The reply code is designated by the <code>replyCode</code> property, and the reply text 178 * is retrieved from the <code>replyText</code> ResourceBundle, using the reply code as the key. 179 * 180 * @param session - the Session 181 * @param replyCode - the reply code 182 * @param args - the optional message arguments; defaults to [] 183 * @throws AssertionError - if session is null 184 * @see MessageFormat 185 */ 186 protected void sendReply(Session session, int replyCode, List args) { 187 sendReply(session, replyCode, Integer.toString(replyCode), args); 188 } 189 190 /** 191 * Handle the exception caught during handleCommand() 192 * 193 * @param command - the Command 194 * @param session - the Session 195 * @param exception - the caught exception 196 * @param replyCode - the reply code that should be sent back 197 */ 198 private void handleException(Command command, Session session, Throwable exception, int replyCode) { 199 LOG.warn("Error handling command: " + command + "; " + exception, exception); 200 sendReply(session, replyCode); 201 } 202 203 /** 204 * Handle the exception caught during handleCommand() 205 * 206 * @param command - the Command 207 * @param session - the Session 208 * @param exception - the caught exception 209 * @param replyCode - the reply code that should be sent back 210 * @param arg - the arg for the reply (message) 211 */ 212 private void handleFileSystemException(Command command, Session session, FileSystemException exception, int replyCode, Object arg) { 213 LOG.warn("Error handling command: " + command + "; " + exception, exception); 214 sendReply(session, replyCode, exception.getMessageKey(), Collections.singletonList(arg)); 215 } 216 217 /** 218 * Return the value of the named attribute within the session. 219 * 220 * @param session - the Session 221 * @param name - the name of the session attribute to retrieve 222 * @return the value of the named session attribute 223 * @throws IllegalStateException - if the Session does not contain the named attribute 224 */ 225 protected Object getRequiredSessionAttribute(Session session, String name) { 226 Object value = session.getAttribute(name); 227 if (value == null) { 228 throw new IllegalStateException("Session missing required attribute [" + name + "]"); 229 } 230 return value; 231 } 232 233 /** 234 * Verify that the current user (if any) has already logged in successfully. 235 * 236 * @param session - the Session 237 */ 238 protected void verifyLoggedIn(Session session) { 239 if (getUserAccount(session) == null) { 240 throw new NotLoggedInException("User has not logged in"); 241 } 242 } 243 244 /** 245 * @param session - the Session 246 * @return the UserAccount stored in the specified session; may be null 247 */ 248 protected UserAccount getUserAccount(Session session) { 249 return (UserAccount) session.getAttribute(SessionKeys.USER_ACCOUNT); 250 } 251 252 /** 253 * Verify that the specified condition related to the file system is true, 254 * otherwise throw a FileSystemException. 255 * 256 * @param condition - the condition that must be true 257 * @param path - the path involved in the operation; this will be included in the 258 * error message if the condition is not true. 259 * @param messageKey - the message key for the exception message 260 * @throws FileSystemException - if the condition is not true 261 */ 262 protected void verifyFileSystemCondition(boolean condition, String path, String messageKey) { 263 if (!condition) { 264 throw new FileSystemException(path, messageKey); 265 } 266 } 267 268 /** 269 * Verify that the current user has execute permission to the specified path 270 * 271 * @param session - the Session 272 * @param path - the file system path 273 * @throws FileSystemException - if the condition is not true 274 */ 275 protected void verifyExecutePermission(Session session, String path) { 276 UserAccount userAccount = getUserAccount(session); 277 FileSystemEntry entry = getFileSystem().getEntry(path); 278 verifyFileSystemCondition(userAccount.canExecute(entry), path, "filesystem.cannotExecute"); 279 } 280 281 /** 282 * Verify that the current user has write permission to the specified path 283 * 284 * @param session - the Session 285 * @param path - the file system path 286 * @throws FileSystemException - if the condition is not true 287 */ 288 protected void verifyWritePermission(Session session, String path) { 289 UserAccount userAccount = getUserAccount(session); 290 FileSystemEntry entry = getFileSystem().getEntry(path); 291 verifyFileSystemCondition(userAccount.canWrite(entry), path, "filesystem.cannotWrite"); 292 } 293 294 /** 295 * Verify that the current user has read permission to the specified path 296 * 297 * @param session - the Session 298 * @param path - the file system path 299 * @throws FileSystemException - if the condition is not true 300 */ 301 protected void verifyReadPermission(Session session, String path) { 302 UserAccount userAccount = getUserAccount(session); 303 FileSystemEntry entry = getFileSystem().getEntry(path); 304 verifyFileSystemCondition(userAccount.canRead(entry), path, "filesystem.cannotRead"); 305 } 306 307 /** 308 * Return the full, absolute path for the specified abstract pathname. 309 * If path is null, return the current directory (stored in the session). If 310 * path represents an absolute path, then return path as is. Otherwise, path 311 * is relative, so assemble the full path from the current directory 312 * and the specified relative path. 313 * 314 * @param session - the Session 315 * @param path - the abstract pathname; may be null 316 * @return the resulting full, absolute path 317 */ 318 protected String getRealPath(Session session, String path) { 319 String currentDirectory = (String) session.getAttribute(SessionKeys.CURRENT_DIRECTORY); 320 if (path == null) { 321 return currentDirectory; 322 } 323 if (getFileSystem().isAbsolute(path)) { 324 return path; 325 } 326 return getFileSystem().path(currentDirectory, path); 327 } 328 329 /** 330 * Return the end-of-line character(s) used when building multi-line responses 331 * 332 * @return "\r\n" 333 */ 334 protected String endOfLine() { 335 return "\r\n"; 336 } 337 338 private String getTextForKey(String key) { 339 String msgKey = (key != null) ? key : INTERNAL_ERROR_KEY; 340 try { 341 return getReplyTextBundle().getString(msgKey); 342 } 343 catch (MissingResourceException e) { 344 // No reply text is mapped for the specified key 345 LOG.warn("No reply text defined for key [" + msgKey + "]"); 346 return null; 347 } 348 } 349 350 // ------------------------------------------------------------------------- 351 // Login Support (used by USER and PASS commands) 352 // ------------------------------------------------------------------------- 353 354 /** 355 * Validate the UserAccount for the specified username. If valid, return true. If the UserAccount does 356 * not exist or is invalid, log an error message, send back a reply code of 530 with an appropriate 357 * error message, and return false. A UserAccount is considered invalid if the homeDirectory property 358 * is not set or is set to a non-existent directory. 359 * 360 * @param username - the username 361 * @param session - the session; used to send back an error reply if necessary 362 * @return true only if the UserAccount for the named user is valid 363 */ 364 protected boolean validateUserAccount(String username, Session session) { 365 UserAccount userAccount = serverConfiguration.getUserAccount(username); 366 if (userAccount == null || !userAccount.isValid()) { 367 LOG.error("UserAccount missing or not valid for username [" + username + "]: " + userAccount); 368 sendReply(session, ReplyCodes.USER_ACCOUNT_NOT_VALID, "login.userAccountNotValid", list(username)); 369 return false; 370 } 371 372 String home = userAccount.getHomeDirectory(); 373 if (!getFileSystem().isDirectory(home)) { 374 LOG.error("Home directory configured for username [" + username + "] is not valid: " + home); 375 sendReply(session, ReplyCodes.USER_ACCOUNT_NOT_VALID, "login.homeDirectoryNotValid", list(username, home)); 376 return false; 377 } 378 379 return true; 380 } 381 382 /** 383 * Log in the specified user for the current session. Send back a reply of 230 with a message indicated 384 * by the replyMessageKey and set the UserAccount and current directory (homeDirectory) in the session. 385 * 386 * @param userAccount - the userAccount for the user to be logged in 387 * @param session - the session 388 * @param replyCode - the reply code to send 389 * @param replyMessageKey - the message key for the reply text 390 */ 391 protected void login(UserAccount userAccount, Session session, int replyCode, String replyMessageKey) { 392 sendReply(session, replyCode, replyMessageKey); 393 session.setAttribute(SessionKeys.USER_ACCOUNT, userAccount); 394 session.setAttribute(SessionKeys.CURRENT_DIRECTORY, userAccount.getHomeDirectory()); 395 } 396 397 /** 398 * Convenience method to return a List with the specified single item 399 * 400 * @param item - the single item in the returned List 401 * @return a new List with that single item 402 */ 403 protected List list(Object item) { 404 return Collections.singletonList(item); 405 } 406 407 /** 408 * Convenience method to return a List with the specified two items 409 * 410 * @param item1 - the first item in the returned List 411 * @param item2 - the second item in the returned List 412 * @return a new List with the specified items 413 */ 414 protected List list(Object item1, Object item2) { 415 List list = new ArrayList(2); 416 list.add(item1); 417 list.add(item2); 418 return list; 419 } 420 421 /** 422 * Return true if the specified string is null or empty 423 * 424 * @param string - the String to check; may be null 425 * @return true only if the specified String is null or empyt 426 */ 427 protected boolean notNullOrEmpty(String string) { 428 return string != null && string.length() > 0; 429 } 430 431 /** 432 * Return the string unless it is null or empty, in which case return the defaultString. 433 * 434 * @param string - the String to check; may be null 435 * @param defaultString - the value to return if string is null or empty 436 * @return string if not null and not empty; otherwise return defaultString 437 */ 438 protected String defaultIfNullOrEmpty(String string, String defaultString) { 439 return (notNullOrEmpty(string) ? string : defaultString); 440 } 441 442 }