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