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