/*
* ------------------------------------------------------------------------------
* Hermes FTP Server
* Copyright (c) 2005-2014 Lars Behnke
* ------------------------------------------------------------------------------
*
* This file is part of Hermes FTP Server.
*
* Hermes FTP Server is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* Hermes FTP Server is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Hermes FTP Server; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
* ------------------------------------------------------------------------------
*/
package com.apporiented.hermesftp.client;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.net.ServerSocketFactory;
import com.apporiented.hermesftp.common.FtpConstants;
import com.apporiented.hermesftp.utils.NetUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
// CHECKSTYLE:OFF
/**
* FTP test client for test purposes.
*
* @author Lars Behnke
*/
public class FtpTestClient {
/**
* On Linux ports below 1024 can only be bound by root.
*/
private static final int TEST_FTP_PORT = 2121;
private static final int LOG_LINE_LENGTH = 80;
private static Log log = LogFactory.getLog(FtpTestClient.class);
private PrintWriter out;
private BufferedReader in;
private String server;
private InputStream transIs;
private OutputStream transOut;
private Socket passiveModeSocket;
private ServerSocket activeModeServerSocket;
private Socket serverSocket;
private StringBuffer textBuffer;
private byte[] rawBuffer;
private final Object lock = new Object();
/**
* Returns the text data.
*
* @return The text data.
*/
public String getTextData() {
return textBuffer.toString();
}
/**
* Returns the raw data.
*
* @return The text data.
*/
public byte[] getRawData() {
return rawBuffer;
}
/**
* Opens a anonymous FTP connection.
*
* @throws IOException Error on connection.
*/
public void openConnection() throws IOException {
openConnection("anonymous", "my@mail");
}
/**
* Opens a FTP connection.
*
* @param user The user name.
* @param pass The user password.
* @throws IOException Error on connection.
*/
public void openConnection(String user, String pass) throws IOException {
openConnection(null, user, pass, TEST_FTP_PORT);
}
/**
* Closes the FTP connection.
*/
public void closeConnection() {
try {
if (serverSocket != null) {
serverSocket.close();
serverSocket = null;
}
} catch (IOException e) {
log.debug(e.toString());
}
try {
if (passiveModeSocket != null) {
passiveModeSocket.close();
passiveModeSocket = null;
}
} catch (IOException e) {
log.debug(e.toString());
}
}
/**
* Opening a connection to the FTP server.
*
* @param svr The server name. If null is passed, the local machine is used.
* @param user The user name.
* @param pass The user password.
* @throws IOException Error on connection.
*/
public void openConnection(String svr, String user, String pass) throws IOException {
openConnection(svr, user, pass, TEST_FTP_PORT);
}
/**
* Opening a connection to the FTP server.
*
* @param svr The server name. If null is passed, the local machine is used.
* @param user The user name.
* @param pass The user password.
* @param port FTP port.
* @throws IOException Error on connection.
*/
public void openConnection(String svr, String user, String pass, int port) throws IOException {
this.server = svr;
if (server == null || server.startsWith("127.0.0.")) {
this.server = NetUtils.getMachineAddress(true).getHostAddress();
}
serverSocket = new Socket(server, port);
in = new BufferedReader(new InputStreamReader(serverSocket.getInputStream()));
out = new PrintWriter(serverSocket.getOutputStream(), true);
getResponse();
sendAndReceive("USER " + user);
sendAndReceive("PASS " + pass);
}
public String openPassiveMode() throws IOException {
sendCommand("PASV");
String response = getResponse();
int parentStart = response.lastIndexOf('(');
int parentEnd = response.lastIndexOf(')');
String pasv = response.substring(parentStart + 1, parentEnd);
StringTokenizer st = new StringTokenizer(pasv, ",");
int[] iPs = new int[8];
for (int i = 0; st.hasMoreTokens(); i++) {
iPs[i] = Integer.valueOf(st.nextToken());
}
int port = (iPs[4] << FtpConstants.BYTE_LENGTH) + iPs[5];
resetDataSockets();
passiveModeSocket = new Socket(server, port);
return response;
}
public String openActiveMode() throws IOException {
InetAddress addr = NetUtils.getMachineAddress(true);
String addrStr = addr != null ? addr.getHostAddress() : "127.0.0.1";
ServerSocket sock = ServerSocketFactory.getDefault().createServerSocket(0, 1, addr);
sock.setSoTimeout(10000);
Pattern pattern = Pattern.compile("^([0-9]+)\\.([0-9]+)\\.([0-9]+)\\.([0-9]+)$");
Matcher matcher = pattern.matcher(addrStr);
if (!matcher.matches()) {
throw new IOException("Invalid address: " + addrStr);
}
int p1 = (sock.getLocalPort() >>> FtpConstants.BYTE_LENGTH) & FtpConstants.BYTE_MASK;
int p2 = sock.getLocalPort() & FtpConstants.BYTE_MASK;
StringBuffer sb = new StringBuffer();
sb.append("PORT ");
sb.append(matcher.group(1));
sb.append(",");
sb.append(matcher.group(2));
sb.append(",");
sb.append(matcher.group(3));
sb.append(",");
sb.append(matcher.group(4));
sb.append(",");
sb.append(p1);
sb.append(",");
sb.append(p2);
resetDataSockets();
activeModeServerSocket = sock;
sendCommand(sb.toString());
return getResponse();
}
public String openExtendedPassiveMode() throws IOException {
sendCommand("EPSV");
String response = getResponse();
Pattern pattern = Pattern.compile("^.*\\(\\|\\|\\|([0-9]+)\\|\\).*$");
Matcher matcher = pattern.matcher(response);
int port = 0;
if (matcher.matches()) {
port = Integer.parseInt(matcher.group(1));
}
resetDataSockets();
passiveModeSocket = new Socket(server, port);
return response;
}
private void initializeIOStreams() throws IOException {
if (passiveModeSocket != null) {
transIs = passiveModeSocket.getInputStream();
transOut = passiveModeSocket.getOutputStream();
} else if (activeModeServerSocket != null) {
Socket socket = activeModeServerSocket.accept();
transIs = socket.getInputStream();
transOut = socket.getOutputStream();
} else {
throw new IOException("IO streams have not been initialized");
}
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
private boolean isServerSocketAvailable() {
return passiveModeSocket != null || activeModeServerSocket != null;
}
private void resetDataSockets() throws IOException {
if (passiveModeSocket != null) {
passiveModeSocket.close();
passiveModeSocket = null;
}
if (activeModeServerSocket != null) {
activeModeServerSocket.close();
activeModeServerSocket = null;
}
}
public String openExtendedActiveMode() throws IOException {
StringBuffer params = new StringBuffer();
params.append("|1|");
InetAddress addr = NetUtils.getMachineAddress(true);
ServerSocket serverSocket = ServerSocketFactory.getDefault().createServerSocket(0, 1, addr);
params.append(addr.getHostAddress());
params.append("|");
params.append(serverSocket.getLocalPort());
params.append("|");
sendCommand("EPRT " + params.toString());
String response = getResponse();
if (passiveModeSocket != null) {
passiveModeSocket.close();
}
activeModeServerSocket = serverSocket;
return response;
}
/**
* Lists the content of the passed path.
*
* @param f The path.
* @return The content as string.
* @throws IOException Error on data transfer.
*/
public String list(String f) throws IOException {
String response;
if (!isServerSocketAvailable()) {
openPassiveMode();
}
textBuffer = new StringBuffer();
if (f == null) {
sendCommand("LIST");
} else {
sendCommand("LIST " + f);
}
String res = getResponse();
if (res.startsWith("150")) {
initializeIOStreams();
} else {
return res;
}
BufferedReader in = new BufferedReader(new InputStreamReader(transIs, "ISO-8859-1"));
TextReceiver l = new TextReceiver(in, false);
executeReceive(l);
resetDataSockets();
response = getResponse();
return response;
}
/**
* Lists the current folder.
*
* @return The content.
* @throws IOException Error on data transfer.
*/
public String list() throws IOException {
return list(null);
}
/**
* Retrieves a text file.
*
* @param filename The filename.
* @return The content of the text file.
* @throws IOException Error on data transfer.
*/
public String retrieveText(String filename) throws IOException {
String response;
sendAndReceive("TYPE A");
if (!isServerSocketAvailable()) {
openPassiveMode();
}
textBuffer = new StringBuffer();
response = sendAndReceive("RETR " + filename);
if (!response.startsWith("150")) {
return response;
}
initializeIOStreams();
BufferedReader in = new BufferedReader(new InputStreamReader(transIs, "ISO-8859-1"));
TextReceiver l = new TextReceiver(in, false);
executeReceive(l);
response = getResponse();
resetDataSockets();
return response;
}
/**
* Retrieves a text file.
*
* @param filename The filename.
* @return Size of file.
* @throws IOException Error on data transfer.
*/
public int retrieveBigText(String filename) throws IOException {
String response;
sendAndReceive("TYPE A");
if (!isServerSocketAvailable()) {
openPassiveMode();
}
textBuffer = new StringBuffer();
response = sendAndReceive("RETR " + filename);
if (response.startsWith("150")) {
initializeIOStreams();
} else {
return 0;
}
BufferedReader in = new BufferedReader(new InputStreamReader(transIs, "ISO-8859-1"));
TextReceiver l = new TextReceiver(in, true);
executeReceive(l);
getResponse();
resetDataSockets();
return l.getCount();
}
private void executeReceive(TextReceiver l) {
Thread t = new Thread(l);
t.start();
while (t.isAlive()) {
Thread.yield();
}
}
/**
* Retrieves a raw data file.
*
* @param filename The filename.
* @return The content of the data file.
* @throws IOException Error on data transfer.
*/
public String retrieveRaw(String filename) throws IOException {
String response;
if (!isServerSocketAvailable()) {
openPassiveMode();
}
rawBuffer = null;
response = sendAndReceive("RETR " + filename);
if (!response.startsWith("150")) {
return response;
}
initializeIOStreams();
RawReceiver l = new RawReceiver(transIs);
executeReceive(l);
response = getResponse();
resetDataSockets();
return response;
}
private void executeReceive(RawReceiver l) {
Thread t = new Thread(l);
t.start();
while (t.isAlive()) {
Thread.yield();
}
}
/**
* Stores a text file on the remote system.
*
* @param filename The filename.
* @param textToStore The text to be stored.
* @return The response.
* @throws IOException Error on data transfer.
*/
public String storeText(String filename, String textToStore) throws IOException {
return storeText(filename, textToStore, false);
}
/**
* Appends text to an text file.
*
* @param filename The filename.
* @param textToStore The text to append.
* @return The server response.
* @throws IOException Error on data transfer.
*/
public String appendText(String filename, String textToStore) throws IOException {
return storeText(filename, textToStore, true);
}
private String storeText(String filename, String textToStore, boolean append) throws IOException {
String response;
sendAndReceive("TYPE A");
if (!isServerSocketAvailable()) {
openPassiveMode();
}
textBuffer = new StringBuffer();
if (append) {
sendCommand("APPE" + filename);
} else {
sendCommand("STOR " + filename);
}
response = getResponse();
if (!response.startsWith("150")) {
return response;
}
initializeIOStreams();
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(transOut, "ISO-8859-1"));
TextSender l = new TextSender(out, textToStore);
executeSend(l);
response = getResponse();
// lab
resetDataSockets();
return response;
}
/**
* Stores a file of a given size. The content is arbitrary.
*
* @param filename Filename.
* @param size Size of the file.
* @return Response.
* @throws IOException
*/
public String storeBigText(String filename, int size) throws IOException {
String response;
// openPassiveMode();
response = sendAndReceive("TYPE A");
if (!isServerSocketAvailable()) {
openPassiveMode();
}
textBuffer = new StringBuffer();
sendCommand("STOR " + filename);
response = getResponse();
if (!response.startsWith("150")) {
return response;
}
initializeIOStreams();
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(transOut, "ISO-8859-1"));
TextSender l = new TextSender(out, size);
executeSend(l);
response = getResponse();
resetDataSockets();
return response;
}
private void executeSend(TextSender l) {
Thread t = new Thread(l);
t.start();
while (t.isAlive()) {
Thread.yield();
}
}
/**
* Stores a data file on the remote system.
*
* @param filename The filename.
* @param data The Data to be stored.
* @return The response.
* @throws IOException Error on data transfer.
*/
public String storeRaw(String filename, byte[] data) throws IOException {
return storeRaw(filename, data, false);
}
/**
* Appends text to an data file.
*
* @param filename The filename.
* @param data The data to append.
* @return The server response.
* @throws IOException Error on data transfer.
*/
public String appendRaw(String filename, byte[] data) throws IOException {
return storeRaw(filename, data, true);
}
private String storeRaw(String filename, byte[] data, boolean append) throws IOException {
String response;
//response = sendAndReceive("TYPE I");
if (!isServerSocketAvailable()) {
openPassiveMode();
}
if (append) {
sendCommand("APPE" + filename);
} else {
sendCommand("STOR " + filename);
}
response = getResponse();
if (!response.startsWith("150")) {
return response;
}
initializeIOStreams();
BufferedOutputStream out = new BufferedOutputStream(transOut);
RawSender l = new RawSender(out, data);
executeSend(l);
response = getResponse();
resetDataSockets();
return response;
}
private void executeSend(RawSender l) {
Thread t = new Thread(l);
t.start();
while (t.isAlive()) {
Thread.yield();
}
}
/**
* Sends a command string to the server.
*
* @param cmd The command.
* @return The server response.
* @throws IOException Error on data transfer.
*/
public String sendAndReceive(String cmd) throws IOException {
sendCommand(cmd);
return getResponse();
}
private String getResponse() throws IOException {
return getResponse(in);
}
private String getResponse(BufferedReader in) throws IOException {
StringBuffer sb = new StringBuffer();
boolean done;
do {
String line = in.readLine();
sb.append(line).append("\n");
int idx = 0;
done = Character.isDigit(line.charAt(idx++)) && Character.isDigit(line.charAt(idx++))
&& Character.isDigit(line.charAt(idx++)) && line.charAt(idx++) == ' ';
} while (!done);
return sb.toString().trim();
}
/**
* Send command and wait for resonse.
*/
private void sendCommand(String command) throws IOException {
// log.info("-> " + command);
out.println(command);
}
/**
* Listens to server socket.
*
* @author Lars Behnke
*/
private class TextReceiver implements Runnable {
private BufferedReader reader;
private int count;
private boolean countOnly;
/**
* Constructor.
*
* @param reader The reader.
* @param countOnly True, if only the text size matters.
*/
public TextReceiver(BufferedReader reader, boolean countOnly) {
this.countOnly = countOnly;
this.reader = reader;
}
/**
* {@inheritDoc}
*/
public void run() {
synchronized (lock) {
try {
char[] buffer = new char[4096];
int count;
while ((count = reader.read(buffer)) != -1) {
if (!countOnly) {
String logLine = new String(buffer, 0, count);
if (textBuffer.length() > 0) {
textBuffer.append(System.getProperty("line.separator"));
}
log.info(logLine);
textBuffer.append(logLine);
if (log.isTraceEnabled()) {
if (count >= LOG_LINE_LENGTH) {
logLine = logLine.substring(0, LOG_LINE_LENGTH) + " ["
+ (logLine.length() - LOG_LINE_LENGTH) + " chars more]";
}
log.trace("<==: " + logLine.trim());
}
}
this.count += count;
}
if (countOnly) {
log.trace("<==: " + this.count + " characters read");
}
reader.close();
} catch (IOException e) {
log.error(e);
}
lock.notifyAll();
}
}
/**
* @return Size of file.
*/
public int getCount() {
return count;
}
}
/**
* Sends data to server socket.
*
* @author Lars Behnke
*/
private class TextSender implements Runnable {
private BufferedWriter writer;
private String textToSend;
private int textSize;
/**
* Constructor.
*
* @param writer The writer.
* @param textToSend Text to send.
*/
public TextSender(BufferedWriter writer, String textToSend) {
this.writer = writer;
this.textToSend = textToSend;
this.textSize = -1;
}
/**
* Constructor.
*
* @param writer The writer.
* @param size Size of the test string.
*/
public TextSender(BufferedWriter writer, int size) {
this.writer = writer;
this.textSize = size;
}
/**
* @see java.lang.Runnable#run()
*/
public void run() {
synchronized (lock) {
try {
if (textSize == -1 && textToSend != null) {
if (log.isTraceEnabled()) {
String x;
if (textToSend.length() >= LOG_LINE_LENGTH) {
x = textToSend.substring(0, LOG_LINE_LENGTH) + " ["
+ (textToSend.length() - LOG_LINE_LENGTH) + " chars more]";
} else {
x = textToSend;
}
log.trace("==>: " + x.trim());
}
writer.write(textToSend);
writer.flush();
} else if (textSize >= 0) {
if (log.isTraceEnabled()) {
log.trace("==>: Sending test text, length: " + textSize);
}
for (int i = 0; i < textSize; i++) {
writer.write('X');
}
}
writer.flush();
//if (passiveModeSocket != null) {
// passiveModeSocket.shutdownOutput();
//}
writer.close();
} catch (IOException e) {
log.error(e);
}
lock.notifyAll();
}
}
}
/**
* Sends data to server socket.
*
* @author Lars Behnke
*/
private class RawSender implements Runnable {
private BufferedOutputStream os;
private byte[] dataToSend;
/**
* Constructor.
*
* @param os The output stream.
* @param dataToSend Data to send.
*/
public RawSender(BufferedOutputStream os, byte[] dataToSend) {
this.os = os;
this.dataToSend = dataToSend;
}
/**
* @see java.lang.Runnable#run()
*/
public void run() {
synchronized (lock) {
try {
log.trace("==>: " + dataToSend.length + " bytes");
os.write(dataToSend);
os.flush();
os.close();
} catch (IOException e) {
log.error(e);
} finally {
lock.notifyAll();
}
}
}
}
/**
* Raw data receiver that listens to server socket.
*
* @author Lars Behnke
*/
private class RawReceiver implements Runnable {
private InputStream is;
/**
* Constructor.
*
* @param is The input stream.
*/
public RawReceiver(InputStream is) {
this.is = is;
}
/**
* {@inheritDoc}
*/
public void run() {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int count;
synchronized (lock) {
try {
while ((count = is.read(buffer)) >= 0) {
log.trace("<==: " + count + " bytes");
baos.write(buffer, 0, count);
}
rawBuffer = baos.toByteArray();
} catch (IOException e) {
log.error(e);
} finally {
lock.notifyAll();
}
}
}
}
}
// CHECKSTYLE:ON