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