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