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