/* | |
* Copyright 2007 the original author or authors. | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
*/ | |
package org.mockftpserver.core.session; | |
import org.slf4j.Logger; | |
import org.slf4j.LoggerFactory; | |
import org.mockftpserver.core.MockFtpServerException; | |
import org.mockftpserver.core.command.Command; | |
import org.mockftpserver.core.command.CommandHandler; | |
import org.mockftpserver.core.command.CommandNames; | |
import org.mockftpserver.core.socket.DefaultServerSocketFactory; | |
import org.mockftpserver.core.socket.DefaultSocketFactory; | |
import org.mockftpserver.core.socket.ServerSocketFactory; | |
import org.mockftpserver.core.socket.SocketFactory; | |
import org.mockftpserver.core.util.Assert; | |
import org.mockftpserver.core.util.AssertFailedException; | |
import java.io.BufferedReader; | |
import java.io.ByteArrayOutputStream; | |
import java.io.IOException; | |
import java.io.InputStream; | |
import java.io.InputStreamReader; | |
import java.io.OutputStream; | |
import java.io.PrintWriter; | |
import java.io.Writer; | |
import java.net.InetAddress; | |
import java.net.ServerSocket; | |
import java.net.Socket; | |
import java.net.SocketTimeoutException; | |
import java.util.ArrayList; | |
import java.util.HashMap; | |
import java.util.List; | |
import java.util.Map; | |
import java.util.Set; | |
import java.util.StringTokenizer; | |
/** | |
* Default implementation of the {@link Session} interface. | |
* | |
* @author Chris Mair | |
* @version $Revision$ - $Date$ | |
*/ | |
public class DefaultSession implements Session { | |
private static final Logger LOG = LoggerFactory.getLogger(DefaultSession.class); | |
private static final String END_OF_LINE = "\r\n"; | |
protected static final int DEFAULT_CLIENT_DATA_PORT = 21; | |
protected SocketFactory socketFactory = new DefaultSocketFactory(); | |
protected ServerSocketFactory serverSocketFactory = new DefaultServerSocketFactory(); | |
private BufferedReader controlConnectionReader; | |
private Writer controlConnectionWriter; | |
private Socket controlSocket; | |
private Socket dataSocket; | |
ServerSocket passiveModeDataSocket; // non-private for testing | |
private InputStream dataInputStream; | |
private OutputStream dataOutputStream; | |
private Map commandHandlers; | |
private int clientDataPort = DEFAULT_CLIENT_DATA_PORT; | |
private InetAddress clientHost; | |
private InetAddress serverHost; | |
private Map attributes = new HashMap(); | |
private volatile boolean terminate = false; | |
/** | |
* Create a new initialized instance | |
* | |
* @param controlSocket - the control connection socket | |
* @param commandHandlers - the Map of command name -> CommandHandler. It is assumed that the | |
* command names are all normalized to upper case. See {@link Command#normalizeName(String)}. | |
*/ | |
public DefaultSession(Socket controlSocket, Map commandHandlers) { | |
Assert.notNull(controlSocket, "controlSocket"); | |
Assert.notNull(commandHandlers, "commandHandlers"); | |
this.controlSocket = controlSocket; | |
this.commandHandlers = commandHandlers; | |
this.serverHost = controlSocket.getLocalAddress(); | |
} | |
/** | |
* Return the InetAddress representing the client host for this session | |
* | |
* @return the client host | |
* @see org.mockftpserver.core.session.Session#getClientHost() | |
*/ | |
public InetAddress getClientHost() { | |
return controlSocket.getInetAddress(); | |
} | |
/** | |
* Return the InetAddress representing the server host for this session | |
* | |
* @return the server host | |
* @see org.mockftpserver.core.session.Session#getServerHost() | |
*/ | |
public InetAddress getServerHost() { | |
return serverHost; | |
} | |
/** | |
* Send the specified reply code and text across the control connection. | |
* The reply text is trimmed before being sent. | |
* | |
* @param code - the reply code | |
* @param text - the reply text to send; may be null | |
*/ | |
public void sendReply(int code, String text) { | |
assertValidReplyCode(code); | |
StringBuffer buffer = new StringBuffer(Integer.toString(code)); | |
if (text != null && text.length() > 0) { | |
String replyText = text.trim(); | |
if (replyText.indexOf("\n") != -1) { | |
int lastIndex = replyText.lastIndexOf("\n"); | |
buffer.append("-"); | |
for (int i = 0; i < replyText.length(); i++) { | |
char c = replyText.charAt(i); | |
buffer.append(c); | |
if (i == lastIndex) { | |
buffer.append(Integer.toString(code)); | |
buffer.append(" "); | |
} | |
} | |
} else { | |
buffer.append(" "); | |
buffer.append(replyText); | |
} | |
} | |
LOG.debug("Sending Reply [" + buffer.toString() + "]"); | |
writeLineToControlConnection(buffer.toString()); | |
} | |
/** | |
* @see org.mockftpserver.core.session.Session#openDataConnection() | |
*/ | |
public void openDataConnection() { | |
try { | |
if (passiveModeDataSocket != null) { | |
LOG.debug("Waiting for (passive mode) client connection from client host [" + clientHost | |
+ "] on port " + passiveModeDataSocket.getLocalPort()); | |
// TODO set socket timeout | |
try { | |
dataSocket = passiveModeDataSocket.accept(); | |
LOG.debug("Successful (passive mode) client connection to port " | |
+ passiveModeDataSocket.getLocalPort()); | |
} | |
catch (SocketTimeoutException e) { | |
throw new MockFtpServerException(e); | |
} | |
} else { | |
Assert.notNull(clientHost, "clientHost"); | |
LOG.debug("Connecting to client host [" + clientHost + "] on data port [" + clientDataPort | |
+ "]"); | |
dataSocket = socketFactory.createSocket(clientHost, clientDataPort); | |
} | |
dataOutputStream = dataSocket.getOutputStream(); | |
dataInputStream = dataSocket.getInputStream(); | |
} | |
catch (IOException e) { | |
throw new MockFtpServerException(e); | |
} | |
} | |
/** | |
* Switch to passive mode | |
* | |
* @return the local port to be connected to by clients for data transfers | |
* @see org.mockftpserver.core.session.Session#switchToPassiveMode() | |
*/ | |
public int switchToPassiveMode() { | |
try { | |
passiveModeDataSocket = serverSocketFactory.createServerSocket(0); | |
return passiveModeDataSocket.getLocalPort(); | |
} | |
catch (IOException e) { | |
throw new MockFtpServerException("Error opening passive mode server data socket", e); | |
} | |
} | |
/** | |
* @see org.mockftpserver.core.session.Session#closeDataConnection() | |
*/ | |
public void closeDataConnection() { | |
try { | |
LOG.debug("Flushing and closing client data socket"); | |
dataOutputStream.flush(); | |
dataOutputStream.close(); | |
dataInputStream.close(); | |
dataSocket.close(); | |
} | |
catch (IOException e) { | |
LOG.error("Error closing client data socket", e); | |
} | |
} | |
/** | |
* Write a single line to the control connection, appending a newline | |
* | |
* @param line - the line to write | |
*/ | |
private void writeLineToControlConnection(String line) { | |
try { | |
controlConnectionWriter.write(line + END_OF_LINE); | |
controlConnectionWriter.flush(); | |
} | |
catch (IOException e) { | |
LOG.error("Error writing to control connection", e); | |
throw new MockFtpServerException("Error writing to control connection", e); | |
} | |
} | |
/** | |
* @see org.mockftpserver.core.session.Session#close() | |
*/ | |
public void close() { | |
LOG.trace("close()"); | |
terminate = true; | |
} | |
/** | |
* @see org.mockftpserver.core.session.Session#sendData(byte[], int) | |
*/ | |
public void sendData(byte[] data, int numBytes) { | |
Assert.notNull(data, "data"); | |
try { | |
dataOutputStream.write(data, 0, numBytes); | |
} | |
catch (IOException e) { | |
throw new MockFtpServerException(e); | |
} | |
} | |
/** | |
* @see org.mockftpserver.core.session.Session#readData() | |
*/ | |
public byte[] readData() { | |
return readData(Integer.MAX_VALUE); | |
} | |
/** | |
* @see org.mockftpserver.core.session.Session#readData() | |
*/ | |
public byte[] readData(int numBytes) { | |
ByteArrayOutputStream bytes = new ByteArrayOutputStream(); | |
int numBytesRead = 0; | |
try { | |
while (numBytesRead < numBytes) { | |
int b = dataInputStream.read(); | |
if (b == -1) { | |
break; | |
} | |
bytes.write(b); | |
numBytesRead++; | |
} | |
return bytes.toByteArray(); | |
} | |
catch (IOException e) { | |
throw new MockFtpServerException(e); | |
} | |
} | |
/** | |
* Wait for and read the command sent from the client on the control connection. | |
* | |
* @return the Command sent from the client; may be null if the session has been closed | |
* <p/> | |
* Package-private to enable testing | |
*/ | |
Command readCommand() { | |
final long socketReadIntervalMilliseconds = 20L; | |
try { | |
while (true) { | |
if (terminate) { | |
return null; | |
} | |
// Don't block; only read command when it is available | |
if (controlConnectionReader.ready()) { | |
String command = controlConnectionReader.readLine(); | |
LOG.info("Received command: [" + command + "]"); | |
return parseCommand(command); | |
} | |
try { | |
Thread.sleep(socketReadIntervalMilliseconds); | |
} | |
catch (InterruptedException e) { | |
throw new MockFtpServerException(e); | |
} | |
} | |
} | |
catch (IOException e) { | |
LOG.error("Read failed", e); | |
throw new MockFtpServerException(e); | |
} | |
} | |
/** | |
* Parse the command String into a Command object | |
* | |
* @param commandString - the command String | |
* @return the Command object parsed from the command String | |
*/ | |
Command parseCommand(String commandString) { | |
Assert.notNullOrEmpty(commandString, "commandString"); | |
List parameters = new ArrayList(); | |
String name; | |
int indexOfFirstSpace = commandString.indexOf(" "); | |
if (indexOfFirstSpace != -1) { | |
name = commandString.substring(0, indexOfFirstSpace); | |
StringTokenizer tokenizer = new StringTokenizer(commandString.substring(indexOfFirstSpace + 1), | |
","); | |
while (tokenizer.hasMoreTokens()) { | |
parameters.add(tokenizer.nextToken()); | |
} | |
} else { | |
name = commandString; | |
} | |
String[] parametersArray = new String[parameters.size()]; | |
return new Command(name, (String[]) parameters.toArray(parametersArray)); | |
} | |
/** | |
* @see org.mockftpserver.core.session.Session#setClientDataHost(java.net.InetAddress) | |
*/ | |
public void setClientDataHost(InetAddress clientHost) { | |
this.clientHost = clientHost; | |
} | |
/** | |
* @see org.mockftpserver.core.session.Session#setClientDataPort(int) | |
*/ | |
public void setClientDataPort(int dataPort) { | |
this.clientDataPort = dataPort; | |
// Clear out any passive data connection mode information | |
if (passiveModeDataSocket != null) { | |
try { | |
this.passiveModeDataSocket.close(); | |
} | |
catch (IOException e) { | |
throw new MockFtpServerException(e); | |
} | |
passiveModeDataSocket = null; | |
} | |
} | |
/** | |
* @see java.lang.Runnable#run() | |
*/ | |
public void run() { | |
try { | |
InputStream inputStream = controlSocket.getInputStream(); | |
OutputStream outputStream = controlSocket.getOutputStream(); | |
controlConnectionReader = new BufferedReader(new InputStreamReader(inputStream)); | |
controlConnectionWriter = new PrintWriter(outputStream, true); | |
LOG.debug("Starting the session..."); | |
CommandHandler connectCommandHandler = (CommandHandler) commandHandlers.get(CommandNames.CONNECT); | |
connectCommandHandler.handleCommand(new Command(CommandNames.CONNECT, new String[0]), this); | |
while (!terminate) { | |
readAndProcessCommand(); | |
} | |
} | |
catch (Exception e) { | |
LOG.error("Error:", e); | |
throw new MockFtpServerException(e); | |
} | |
finally { | |
LOG.debug("Cleaning up the session"); | |
try { | |
controlConnectionReader.close(); | |
controlConnectionWriter.close(); | |
} | |
catch (IOException e) { | |
LOG.error("Error:", e); | |
} | |
LOG.debug("Session stopped."); | |
} | |
} | |
/** | |
* Read and process the next command from the control connection | |
* | |
* @throws Exception - if any error occurs | |
*/ | |
private void readAndProcessCommand() throws Exception { | |
Command command = readCommand(); | |
if (command != null) { | |
String normalizedCommandName = Command.normalizeName(command.getName()); | |
CommandHandler commandHandler = (CommandHandler) commandHandlers.get(normalizedCommandName); | |
if (commandHandler == null) { | |
commandHandler = (CommandHandler) commandHandlers.get(CommandNames.UNSUPPORTED); | |
} | |
Assert.notNull(commandHandler, "CommandHandler for command [" + normalizedCommandName + "]"); | |
commandHandler.handleCommand(command, this); | |
} | |
} | |
/** | |
* Assert that the specified number is a valid reply code | |
* | |
* @param replyCode - the reply code to check | |
*/ | |
private void assertValidReplyCode(int replyCode) { | |
Assert.isTrue(replyCode > 0, "The number [" + replyCode + "] is not a valid reply code"); | |
} | |
/** | |
* Return the attribute value for the specified name. Return null if no attribute value | |
* exists for that name or if the attribute value is null. | |
* | |
* @param name - the attribute name; may not be null | |
* @return the value of the attribute stored under name; may be null | |
* @see org.mockftpserver.core.session.Session#getAttribute(java.lang.String) | |
*/ | |
public Object getAttribute(String name) { | |
Assert.notNull(name, "name"); | |
return attributes.get(name); | |
} | |
/** | |
* Store the value under the specified attribute name. | |
* | |
* @param name - the attribute name; may not be null | |
* @param value - the attribute value; may be null | |
* @see org.mockftpserver.core.session.Session#setAttribute(java.lang.String, java.lang.Object) | |
*/ | |
public void setAttribute(String name, Object value) { | |
Assert.notNull(name, "name"); | |
attributes.put(name, value); | |
} | |
/** | |
* Return the Set of names under which attributes have been stored on this session. | |
* Returns an empty Set if no attribute values are stored. | |
* | |
* @return the Set of attribute names | |
* @see org.mockftpserver.core.session.Session#getAttributeNames() | |
*/ | |
public Set getAttributeNames() { | |
return attributes.keySet(); | |
} | |
/** | |
* Remove the attribute value for the specified name. Do nothing if no attribute | |
* value is stored for the specified name. | |
* | |
* @param name - the attribute name; may not be null | |
* @throws AssertFailedException - if name is null | |
* @see org.mockftpserver.core.session.Session#removeAttribute(java.lang.String) | |
*/ | |
public void removeAttribute(String name) { | |
Assert.notNull(name, "name"); | |
attributes.remove(name); | |
} | |
} |