DefaultSession.java revision 12674b20efcdd79e793d4ca3c7697232658aa036
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.core.session; 17 18import org.apache.log4j.Logger; 19import org.mockftpserver.core.MockFtpServerException; 20import org.mockftpserver.core.command.Command; 21import org.mockftpserver.core.command.CommandHandler; 22import org.mockftpserver.core.command.CommandNames; 23import org.mockftpserver.core.socket.DefaultServerSocketFactory; 24import org.mockftpserver.core.socket.DefaultSocketFactory; 25import org.mockftpserver.core.socket.ServerSocketFactory; 26import org.mockftpserver.core.socket.SocketFactory; 27import org.mockftpserver.core.util.Assert; 28import org.mockftpserver.core.util.AssertFailedException; 29 30import java.io.BufferedReader; 31import java.io.ByteArrayOutputStream; 32import java.io.IOException; 33import java.io.InputStream; 34import java.io.InputStreamReader; 35import java.io.OutputStream; 36import java.io.PrintWriter; 37import java.io.Writer; 38import java.net.InetAddress; 39import java.net.ServerSocket; 40import java.net.Socket; 41import java.net.SocketTimeoutException; 42import java.util.ArrayList; 43import java.util.HashMap; 44import java.util.List; 45import java.util.Map; 46import java.util.Set; 47import java.util.StringTokenizer; 48 49/** 50 * Default implementation of the {@link Session} interface. 51 * 52 * @author Chris Mair 53 * @version $Revision$ - $Date$ 54 */ 55public class DefaultSession implements Session { 56 57 private static final Logger LOG = Logger.getLogger(DefaultSession.class); 58 private static final String END_OF_LINE = "\r\n"; 59 protected static final int DEFAULT_CLIENT_DATA_PORT = 21; 60 61 protected SocketFactory socketFactory = new DefaultSocketFactory(); 62 protected ServerSocketFactory serverSocketFactory = new DefaultServerSocketFactory(); 63 64 private BufferedReader controlConnectionReader; 65 private Writer controlConnectionWriter; 66 private Socket controlSocket; 67 private Socket dataSocket; 68 ServerSocket passiveModeDataSocket; // non-private for testing 69 private InputStream dataInputStream; 70 private OutputStream dataOutputStream; 71 private Map commandHandlers; 72 private int clientDataPort = DEFAULT_CLIENT_DATA_PORT; 73 private InetAddress clientHost; 74 private InetAddress serverHost; 75 private Map attributes = new HashMap(); 76 private volatile boolean terminate = false; 77 78 /** 79 * Create a new initialized instance 80 * 81 * @param controlSocket - the control connection socket 82 * @param commandHandlers - the Map of command name -> CommandHandler. It is assumed that the 83 * command names are all normalized to upper case. See {@link Command#normalizeName(String)}. 84 */ 85 public DefaultSession(Socket controlSocket, Map commandHandlers) { 86 Assert.notNull(controlSocket, "controlSocket"); 87 Assert.notNull(commandHandlers, "commandHandlers"); 88 89 this.controlSocket = controlSocket; 90 this.commandHandlers = commandHandlers; 91 this.serverHost = controlSocket.getLocalAddress(); 92 } 93 94 /** 95 * Return the InetAddress representing the client host for this session 96 * 97 * @return the client host 98 * @see org.mockftpserver.core.session.Session#getClientHost() 99 */ 100 public InetAddress getClientHost() { 101 return controlSocket.getInetAddress(); 102 } 103 104 /** 105 * Return the InetAddress representing the server host for this session 106 * 107 * @return the server host 108 * @see org.mockftpserver.core.session.Session#getServerHost() 109 */ 110 public InetAddress getServerHost() { 111 return serverHost; 112 } 113 114 /** 115 * Send the specified reply code and text across the control connection. 116 * The reply text is trimmed before being sent. 117 * 118 * @param code - the reply code 119 * @param text - the reply text to send; may be null 120 */ 121 public void sendReply(int code, String text) { 122 assertValidReplyCode(code); 123 124 StringBuffer buffer = new StringBuffer(Integer.toString(code)); 125 126 if (text != null && text.length() > 0) { 127 String replyText = text.trim(); 128 if (replyText.indexOf("\n") != -1) { 129 int lastIndex = replyText.lastIndexOf("\n"); 130 buffer.append("-"); 131 for (int i = 0; i < replyText.length(); i++) { 132 char c = replyText.charAt(i); 133 buffer.append(c); 134 if (i == lastIndex) { 135 buffer.append(Integer.toString(code)); 136 buffer.append(" "); 137 } 138 } 139 } else { 140 buffer.append(" "); 141 buffer.append(replyText); 142 } 143 } 144 LOG.debug("Sending Reply [" + buffer.toString() + "]"); 145 writeLineToControlConnection(buffer.toString()); 146 } 147 148 /** 149 * @see org.mockftpserver.core.session.Session#openDataConnection() 150 */ 151 public void openDataConnection() { 152 try { 153 if (passiveModeDataSocket != null) { 154 LOG.debug("Waiting for (passive mode) client connection from client host [" + clientHost 155 + "] on port " + passiveModeDataSocket.getLocalPort()); 156 // TODO set socket timeout 157 try { 158 dataSocket = passiveModeDataSocket.accept(); 159 LOG.debug("Successful (passive mode) client connection to port " 160 + passiveModeDataSocket.getLocalPort()); 161 } 162 catch (SocketTimeoutException e) { 163 throw new MockFtpServerException(e); 164 } 165 } else { 166 Assert.notNull(clientHost, "clientHost"); 167 LOG.debug("Connecting to client host [" + clientHost + "] on data port [" + clientDataPort 168 + "]"); 169 dataSocket = socketFactory.createSocket(clientHost, clientDataPort); 170 } 171 dataOutputStream = dataSocket.getOutputStream(); 172 dataInputStream = dataSocket.getInputStream(); 173 } 174 catch (IOException e) { 175 throw new MockFtpServerException(e); 176 } 177 } 178 179 /** 180 * Switch to passive mode 181 * 182 * @return the local port to be connected to by clients for data transfers 183 * @see org.mockftpserver.core.session.Session#switchToPassiveMode() 184 */ 185 public int switchToPassiveMode() { 186 try { 187 passiveModeDataSocket = serverSocketFactory.createServerSocket(0); 188 return passiveModeDataSocket.getLocalPort(); 189 } 190 catch (IOException e) { 191 throw new MockFtpServerException("Error opening passive mode server data socket", e); 192 } 193 } 194 195 /** 196 * @see org.mockftpserver.core.session.Session#closeDataConnection() 197 */ 198 public void closeDataConnection() { 199 try { 200 LOG.debug("Flushing and closing client data socket"); 201 dataOutputStream.flush(); 202 dataOutputStream.close(); 203 dataInputStream.close(); 204 dataSocket.close(); 205 } 206 catch (IOException e) { 207 LOG.error("Error closing client data socket", e); 208 } 209 } 210 211 /** 212 * Write a single line to the control connection, appending a newline 213 * 214 * @param line - the line to write 215 */ 216 private void writeLineToControlConnection(String line) { 217 try { 218 controlConnectionWriter.write(line + END_OF_LINE); 219 controlConnectionWriter.flush(); 220 } 221 catch (IOException e) { 222 LOG.error("Error writing to control connection", e); 223 throw new MockFtpServerException("Error writing to control connection", e); 224 } 225 } 226 227 /** 228 * @see org.mockftpserver.core.session.Session#close() 229 */ 230 public void close() { 231 LOG.trace("close()"); 232 terminate = true; 233 } 234 235 /** 236 * @see org.mockftpserver.core.session.Session#sendData(byte[], int) 237 */ 238 public void sendData(byte[] data, int numBytes) { 239 Assert.notNull(data, "data"); 240 try { 241 dataOutputStream.write(data, 0, numBytes); 242 } 243 catch (IOException e) { 244 throw new MockFtpServerException(e); 245 } 246 } 247 248 /** 249 * @see org.mockftpserver.core.session.Session#readData() 250 */ 251 public byte[] readData() { 252 253 ByteArrayOutputStream bytes = new ByteArrayOutputStream(); 254 255 try { 256 while (true) { 257 int b = dataInputStream.read(); 258 if (b == -1) { 259 break; 260 } 261 bytes.write(b); 262 } 263 return bytes.toByteArray(); 264 } 265 catch (IOException e) { 266 throw new MockFtpServerException(e); 267 } 268 } 269 270 /** 271 * Wait for and read the command sent from the client on the control connection. 272 * 273 * @return the Command sent from the client; may be null if the session has been closed 274 * <p/> 275 * Package-private to enable testing 276 */ 277 Command readCommand() { 278 279 final long socketReadIntervalMilliseconds = 20L; 280 281 try { 282 while (true) { 283 if (terminate) { 284 return null; 285 } 286 // Don't block; only read command when it is available 287 if (controlConnectionReader.ready()) { 288 String command = controlConnectionReader.readLine(); 289 LOG.info("Received command: [" + command + "]"); 290 return parseCommand(command); 291 } 292 try { 293 Thread.sleep(socketReadIntervalMilliseconds); 294 } 295 catch (InterruptedException e) { 296 throw new MockFtpServerException(e); 297 } 298 } 299 } 300 catch (IOException e) { 301 LOG.error("Read failed", e); 302 throw new MockFtpServerException(e); 303 } 304 } 305 306 /** 307 * Parse the command String into a Command object 308 * 309 * @param commandString - the command String 310 * @return the Command object parsed from the command String 311 */ 312 Command parseCommand(String commandString) { 313 Assert.notNullOrEmpty(commandString, "commandString"); 314 315 List parameters = new ArrayList(); 316 String name; 317 318 int indexOfFirstSpace = commandString.indexOf(" "); 319 if (indexOfFirstSpace != -1) { 320 name = commandString.substring(0, indexOfFirstSpace); 321 StringTokenizer tokenizer = new StringTokenizer(commandString.substring(indexOfFirstSpace + 1), 322 ","); 323 while (tokenizer.hasMoreTokens()) { 324 parameters.add(tokenizer.nextToken()); 325 } 326 } else { 327 name = commandString; 328 } 329 330 String[] parametersArray = new String[parameters.size()]; 331 return new Command(name, (String[]) parameters.toArray(parametersArray)); 332 } 333 334 /** 335 * @see org.mockftpserver.core.session.Session#setClientDataHost(java.net.InetAddress) 336 */ 337 public void setClientDataHost(InetAddress clientHost) { 338 this.clientHost = clientHost; 339 } 340 341 /** 342 * @see org.mockftpserver.core.session.Session#setClientDataPort(int) 343 */ 344 public void setClientDataPort(int dataPort) { 345 this.clientDataPort = dataPort; 346 347 // Clear out any passive data connection mode information 348 if (passiveModeDataSocket != null) { 349 try { 350 this.passiveModeDataSocket.close(); 351 } 352 catch (IOException e) { 353 throw new MockFtpServerException(e); 354 } 355 passiveModeDataSocket = null; 356 } 357 } 358 359 /** 360 * @see java.lang.Runnable#run() 361 */ 362 public void run() { 363 try { 364 365 InputStream inputStream = controlSocket.getInputStream(); 366 OutputStream outputStream = controlSocket.getOutputStream(); 367 controlConnectionReader = new BufferedReader(new InputStreamReader(inputStream)); 368 controlConnectionWriter = new PrintWriter(outputStream, true); 369 370 LOG.debug("Starting the session..."); 371 372 CommandHandler connectCommandHandler = (CommandHandler) commandHandlers.get(CommandNames.CONNECT); 373 connectCommandHandler.handleCommand(new Command(CommandNames.CONNECT, new String[0]), this); 374 375 while (!terminate) { 376 readAndProcessCommand(); 377 } 378 } 379 catch (Exception e) { 380 LOG.error(e); 381 throw new MockFtpServerException(e); 382 } 383 finally { 384 LOG.debug("Cleaning up the session"); 385 try { 386 controlConnectionReader.close(); 387 controlConnectionWriter.close(); 388 } 389 catch (IOException e) { 390 LOG.error(e); 391 } 392 LOG.debug("Session stopped."); 393 } 394 } 395 396 /** 397 * Read and process the next command from the control connection 398 * 399 * @throws Exception - if any error occurs 400 */ 401 private void readAndProcessCommand() throws Exception { 402 403 Command command = readCommand(); 404 if (command != null) { 405 String normalizedCommandName = Command.normalizeName(command.getName()); 406 CommandHandler commandHandler = (CommandHandler) commandHandlers.get(normalizedCommandName); 407 408 if (commandHandler == null) { 409 commandHandler = (CommandHandler) commandHandlers.get(CommandNames.UNSUPPORTED); 410 } 411 412 Assert.notNull(commandHandler, "CommandHandler for command [" + normalizedCommandName + "]"); 413 commandHandler.handleCommand(command, this); 414 } 415 } 416 417 /** 418 * Assert that the specified number is a valid reply code 419 * 420 * @param replyCode - the reply code to check 421 */ 422 private void assertValidReplyCode(int replyCode) { 423 Assert.isTrue(replyCode > 0, "The number [" + replyCode + "] is not a valid reply code"); 424 } 425 426 /** 427 * Return the attribute value for the specified name. Return null if no attribute value 428 * exists for that name or if the attribute value is null. 429 * 430 * @param name - the attribute name; may not be null 431 * @return the value of the attribute stored under name; may be null 432 * @see org.mockftpserver.core.session.Session#getAttribute(java.lang.String) 433 */ 434 public Object getAttribute(String name) { 435 Assert.notNull(name, "name"); 436 return attributes.get(name); 437 } 438 439 /** 440 * Store the value under the specified attribute name. 441 * 442 * @param name - the attribute name; may not be null 443 * @param value - the attribute value; may be null 444 * @see org.mockftpserver.core.session.Session#setAttribute(java.lang.String, java.lang.Object) 445 */ 446 public void setAttribute(String name, Object value) { 447 Assert.notNull(name, "name"); 448 attributes.put(name, value); 449 } 450 451 /** 452 * Return the Set of names under which attributes have been stored on this session. 453 * Returns an empty Set if no attribute values are stored. 454 * 455 * @return the Set of attribute names 456 * @see org.mockftpserver.core.session.Session#getAttributeNames() 457 */ 458 public Set getAttributeNames() { 459 return attributes.keySet(); 460 } 461 462 /** 463 * Remove the attribute value for the specified name. Do nothing if no attribute 464 * value is stored for the specified name. 465 * 466 * @param name - the attribute name; may not be null 467 * @throws AssertFailedException - if name is null 468 * @see org.mockftpserver.core.session.Session#removeAttribute(java.lang.String) 469 */ 470 public void removeAttribute(String name) { 471 Assert.notNull(name, "name"); 472 attributes.remove(name); 473 } 474 475} 476