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