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