Home | History | Annotate | Download | only in session
      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  */
     16 package org.mockftpserver.core.session;
     17 
     18 import org.slf4j.Logger;
     19 import org.slf4j.LoggerFactory;
     20 import org.mockftpserver.core.MockFtpServerException;
     21 import org.mockftpserver.core.command.Command;
     22 import org.mockftpserver.core.command.CommandHandler;
     23 import org.mockftpserver.core.command.CommandNames;
     24 import org.mockftpserver.core.socket.DefaultServerSocketFactory;
     25 import org.mockftpserver.core.socket.DefaultSocketFactory;
     26 import org.mockftpserver.core.socket.ServerSocketFactory;
     27 import org.mockftpserver.core.socket.SocketFactory;
     28 import org.mockftpserver.core.util.Assert;
     29 import org.mockftpserver.core.util.AssertFailedException;
     30 
     31 import java.io.BufferedReader;
     32 import java.io.ByteArrayOutputStream;
     33 import java.io.IOException;
     34 import java.io.InputStream;
     35 import java.io.InputStreamReader;
     36 import java.io.OutputStream;
     37 import java.io.PrintWriter;
     38 import java.io.Writer;
     39 import java.net.InetAddress;
     40 import java.net.ServerSocket;
     41 import java.net.Socket;
     42 import java.net.SocketTimeoutException;
     43 import java.util.ArrayList;
     44 import java.util.HashMap;
     45 import java.util.List;
     46 import java.util.Map;
     47 import java.util.Set;
     48 import java.util.StringTokenizer;
     49 
     50 /**
     51  * Default implementation of the {@link Session} interface.
     52  *
     53  * @author Chris Mair
     54  * @version $Revision$ - $Date$
     55  */
     56 public 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