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