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 return readData(Integer.MAX_VALUE); 253 } 254 255 /** 256 * @see org.mockftpserver.core.session.Session#readData() 257 */ 258 public byte[] readData(int numBytes) { 259 ByteArrayOutputStream bytes = new ByteArrayOutputStream(); 260 int numBytesRead = 0; 261 try { 262 while (numBytesRead < numBytes) { 263 int b = dataInputStream.read(); 264 if (b == -1) { 265 break; 266 } 267 bytes.write(b); 268 numBytesRead++; 269 } 270 return bytes.toByteArray(); 271 } 272 catch (IOException e) { 273 throw new MockFtpServerException(e); 274 } 275 } 276 277 /** 278 * Wait for and read the command sent from the client on the control connection. 279 * 280 * @return the Command sent from the client; may be null if the session has been closed 281 * <p/> 282 * Package-private to enable testing 283 */ 284 Command readCommand() { 285 286 final long socketReadIntervalMilliseconds = 20L; 287 288 try { 289 while (true) { 290 if (terminate) { 291 return null; 292 } 293 // Don't block; only read command when it is available 294 if (controlConnectionReader.ready()) { 295 String command = controlConnectionReader.readLine(); 296 LOG.info("Received command: [" + command + "]"); 297 return parseCommand(command); 298 } 299 try { 300 Thread.sleep(socketReadIntervalMilliseconds); 301 } 302 catch (InterruptedException e) { 303 throw new MockFtpServerException(e); 304 } 305 } 306 } 307 catch (IOException e) { 308 LOG.error("Read failed", e); 309 throw new MockFtpServerException(e); 310 } 311 } 312 313 /** 314 * Parse the command String into a Command object 315 * 316 * @param commandString - the command String 317 * @return the Command object parsed from the command String 318 */ 319 Command parseCommand(String commandString) { 320 Assert.notNullOrEmpty(commandString, "commandString"); 321 322 List parameters = new ArrayList(); 323 String name; 324 325 int indexOfFirstSpace = commandString.indexOf(" "); 326 if (indexOfFirstSpace != -1) { 327 name = commandString.substring(0, indexOfFirstSpace); 328 StringTokenizer tokenizer = new StringTokenizer(commandString.substring(indexOfFirstSpace + 1), 329 ","); 330 while (tokenizer.hasMoreTokens()) { 331 parameters.add(tokenizer.nextToken()); 332 } 333 } else { 334 name = commandString; 335 } 336 337 String[] parametersArray = new String[parameters.size()]; 338 return new Command(name, (String[]) parameters.toArray(parametersArray)); 339 } 340 341 /** 342 * @see org.mockftpserver.core.session.Session#setClientDataHost(java.net.InetAddress) 343 */ 344 public void setClientDataHost(InetAddress clientHost) { 345 this.clientHost = clientHost; 346 } 347 348 /** 349 * @see org.mockftpserver.core.session.Session#setClientDataPort(int) 350 */ 351 public void setClientDataPort(int dataPort) { 352 this.clientDataPort = dataPort; 353 354 // Clear out any passive data connection mode information 355 if (passiveModeDataSocket != null) { 356 try { 357 this.passiveModeDataSocket.close(); 358 } 359 catch (IOException e) { 360 throw new MockFtpServerException(e); 361 } 362 passiveModeDataSocket = null; 363 } 364 } 365 366 /** 367 * @see java.lang.Runnable#run() 368 */ 369 public void run() { 370 try { 371 372 InputStream inputStream = controlSocket.getInputStream(); 373 OutputStream outputStream = controlSocket.getOutputStream(); 374 controlConnectionReader = new BufferedReader(new InputStreamReader(inputStream)); 375 controlConnectionWriter = new PrintWriter(outputStream, true); 376 377 LOG.debug("Starting the session..."); 378 379 CommandHandler connectCommandHandler = (CommandHandler) commandHandlers.get(CommandNames.CONNECT); 380 connectCommandHandler.handleCommand(new Command(CommandNames.CONNECT, new String[0]), this); 381 382 while (!terminate) { 383 readAndProcessCommand(); 384 } 385 } 386 catch (Exception e) { 387 LOG.error(e); 388 throw new MockFtpServerException(e); 389 } 390 finally { 391 LOG.debug("Cleaning up the session"); 392 try { 393 controlConnectionReader.close(); 394 controlConnectionWriter.close(); 395 } 396 catch (IOException e) { 397 LOG.error(e); 398 } 399 LOG.debug("Session stopped."); 400 } 401 } 402 403 /** 404 * Read and process the next command from the control connection 405 * 406 * @throws Exception - if any error occurs 407 */ 408 private void readAndProcessCommand() throws Exception { 409 410 Command command = readCommand(); 411 if (command != null) { 412 String normalizedCommandName = Command.normalizeName(command.getName()); 413 CommandHandler commandHandler = (CommandHandler) commandHandlers.get(normalizedCommandName); 414 415 if (commandHandler == null) { 416 commandHandler = (CommandHandler) commandHandlers.get(CommandNames.UNSUPPORTED); 417 } 418 419 Assert.notNull(commandHandler, "CommandHandler for command [" + normalizedCommandName + "]"); 420 commandHandler.handleCommand(command, this); 421 } 422 } 423 424 /** 425 * Assert that the specified number is a valid reply code 426 * 427 * @param replyCode - the reply code to check 428 */ 429 private void assertValidReplyCode(int replyCode) { 430 Assert.isTrue(replyCode > 0, "The number [" + replyCode + "] is not a valid reply code"); 431 } 432 433 /** 434 * Return the attribute value for the specified name. Return null if no attribute value 435 * exists for that name or if the attribute value is null. 436 * 437 * @param name - the attribute name; may not be null 438 * @return the value of the attribute stored under name; may be null 439 * @see org.mockftpserver.core.session.Session#getAttribute(java.lang.String) 440 */ 441 public Object getAttribute(String name) { 442 Assert.notNull(name, "name"); 443 return attributes.get(name); 444 } 445 446 /** 447 * Store the value under the specified attribute name. 448 * 449 * @param name - the attribute name; may not be null 450 * @param value - the attribute value; may be null 451 * @see org.mockftpserver.core.session.Session#setAttribute(java.lang.String, java.lang.Object) 452 */ 453 public void setAttribute(String name, Object value) { 454 Assert.notNull(name, "name"); 455 attributes.put(name, value); 456 } 457 458 /** 459 * Return the Set of names under which attributes have been stored on this session. 460 * Returns an empty Set if no attribute values are stored. 461 * 462 * @return the Set of attribute names 463 * @see org.mockftpserver.core.session.Session#getAttributeNames() 464 */ 465 public Set getAttributeNames() { 466 return attributes.keySet(); 467 } 468 469 /** 470 * Remove the attribute value for the specified name. Do nothing if no attribute 471 * value is stored for the specified name. 472 * 473 * @param name - the attribute name; may not be null 474 * @throws AssertFailedException - if name is null 475 * @see org.mockftpserver.core.session.Session#removeAttribute(java.lang.String) 476 */ 477 public void removeAttribute(String name) { 478 Assert.notNull(name, "name"); 479 attributes.remove(name); 480 } 481 482} 483