blob: 3a8ada6477491a39aeb925d4d6b3132d31ff8c9d [file] [log] [blame]
/*
* 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);
}
}