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