/**
*
* Java FTP client library.
*
* Copyright (C) 2000-2003 Enterprise Distributed Technologies Ltd
*
* www.enterprisedt.com
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* Bug fixes, suggestions and comments should be sent to bruce@enterprisedt.com
*
* Change Log:
*
* $Log: FTPControlSocket.java,v $
* Revision 1.1.1.1 2005/06/23 15:22:59 smontoro
* hipergate backend
*
* Revision 1.1 2004/02/07 03:15:20 hipergate
* v2.0 pre-alpha
*
* Revision 1.6 2003/05/31 14:53:44 bruceb
* 1.2.2 changes
*
* Revision 1.5 2003/01/29 22:46:08 bruceb
* minor changes
*
* Revision 1.4 2002/11/19 22:01:25 bruceb
* changes for 1.2
*
* Revision 1.3 2001/10/09 20:53:46 bruceb
* Active mode changes
*
* Revision 1.1 2001/10/05 14:42:04 bruceb
* moved from old project
*
*
*/
package com.enterprisedt.net.ftp;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.Socket;
import java.net.ServerSocket;
import java.net.InetAddress;
/**
* Supports client-side FTP operations
*
* @author Bruce Blackshaw
* @version $Revision: 1.1.1.1 $
*
*/
public class FTPControlSocket {
/**
* Revision control id
*/
private static String cvsId = "@(#)$Id: FTPControlSocket.java,v 1.1.1.1 2005/06/23 15:22:59 smontoro Exp $";
/**
* Standard FTP end of line sequence
*/
static final String EOL = "\r\n";
/**
* The control port number for FTP
*/
static final int CONTROL_PORT = 21;
/**
* Used to flag messages
*/
private static final String DEBUG_ARROW = "---> ";
/**
* Start of password message
*/
private static final String PASSWORD_MESSAGE = DEBUG_ARROW + "PASS";
/**
* Controls if responses sent back by the
* server are sent to assigned output stream
*/
private boolean debugResponses = false;
/**
* Output stream debug is written to,
* stdout by default
*/
private PrintWriter log = new PrintWriter(System.out);
/**
* The underlying socket.
*/
private Socket controlSock = null;
/**
* The write that writes to the control socket
*/
private Writer writer = null;
/**
* The reader that reads control data from the
* control socket
*/
private BufferedReader reader = null;
/**
* Constructor. Performs TCP connection and
* sets up reader/writer. Allows different control
* port to be used
*
* @param remoteHost Remote hostname
* @param controlPort port for control stream
* @param millis the length of the timeout, in milliseconds
* @param log the new logging stream
*/
public FTPControlSocket(String remoteHost, int controlPort,
PrintWriter log, int timeout)
throws IOException, FTPException {
setLogStream(log);
// ensure we get debug from initial connection sequence
debugResponses(true);
controlSock = new Socket(remoteHost, controlPort);
setTimeout(timeout);
initStreams();
validateConnection();
// switch off debug - user can switch on from this point
debugResponses(false);
}
/**
* Constructor. Performs TCP connection and
* sets up reader/writer. Allows different control
* port to be used
*
* @param remoteAddr Remote inet address
* @param controlPort port for control stream
* @param millis the length of the timeout, in milliseconds
* @param log the new logging stream
*/
public FTPControlSocket(InetAddress remoteAddr, int controlPort,
PrintWriter log, int timeout)
throws IOException, FTPException {
setLogStream(log);
// ensure we get debug from initial connection sequence
debugResponses(true);
controlSock = new Socket(remoteAddr, controlPort);
setTimeout(timeout);
initStreams();
validateConnection();
// switch off debug - user can switch on from this point
debugResponses(false);
}
/**
* Checks that the standard 220 reply is returned
* following the initiated connection
*/
private void validateConnection()
throws IOException, FTPException {
String reply = readReply();
validateReply(reply, "220");
}
/**
* Obtain the reader/writer streams for this
* connection
*/
private void initStreams()
throws IOException {
// input stream
InputStream is = controlSock.getInputStream();
reader = new BufferedReader(new InputStreamReader(is));
// output stream
OutputStream os = controlSock.getOutputStream();
writer = new OutputStreamWriter(os);
}
/**
* Get the name of the remote host
*
* @return remote host name
*/
String getRemoteHostName() {
InetAddress addr = controlSock.getInetAddress();
return addr.getHostName();
}
/**
* Set the TCP timeout on the underlying control socket.
*
* If a timeout is set, then any operation which
* takes longer than the timeout value will be
* killed with a java.io.InterruptedException.
*
* @param millis The length of the timeout, in milliseconds
*/
void setTimeout(int millis)
throws IOException {
if (controlSock == null)
throw new IllegalStateException(
"Failed to set timeout - no control socket");
controlSock.setSoTimeout(millis);
}
/**
* Quit this FTP session and clean up.
*/
public void logout()
throws IOException {
if (log != null) {
log.flush();
log = null;
}
IOException ex = null;
try {
writer.close();
}
catch (IOException e) {
ex = e;
}
try {
reader.close();
}
catch (IOException e) {
ex = e;
}
try {
controlSock.close();
}
catch (IOException e) {
ex = e;
}
if (ex != null)
throw ex;
}
/**
* Request a data socket be created on the
* server, connect to it and return our
* connected socket.
*
* @param active if true, create in active mode, else
* in passive mode
* @return connected data socket
*/
FTPDataSocket createDataSocket(FTPConnectMode connectMode)
throws IOException, FTPException {
if (connectMode == FTPConnectMode.ACTIVE) {
return new FTPDataSocket(createDataSocketActive());
}
else { // PASV
return new FTPDataSocket(createDataSocketPASV());
}
}
/**
* Request a data socket be created on the Client
* client on any free port, do not connect it to yet.
*
* @return not connected data socket
*/
ServerSocket createDataSocketActive()
throws IOException, FTPException {
// use any available port
ServerSocket socket = new ServerSocket(0);
// get the local address to which the control socket is bound.
InetAddress localhost = controlSock.getLocalAddress();
// send the PORT command to the server
setDataPort(localhost, (short)socket.getLocalPort());
return socket;
}
/**
* Helper method to convert a byte into an unsigned short value
*
* @param value value to convert
* @return the byte value as an unsigned short
*/
private short toUnsignedShort(byte value) {
return ( value < 0 )
? (short) (value + 256)
: (short) value;
}
/**
* Convert a short into a byte array
*
* @param value value to convert
* @return a byte array
*/
protected byte[] toByteArray (short value) {
byte[] bytes = new byte[2];
bytes[0] = (byte) (value >> 8); // bits 1- 8
bytes[1] = (byte) (value & 0x00FF); // bits 9-16
return bytes;
}
/**
* Sets the data port on the server, i.e. sends a PORT
* command
*
* @param host the local host the server will connect to
* @param portNo the port number to connect to
*/
private void setDataPort(InetAddress host, short portNo)
throws IOException, FTPException {
byte[] hostBytes = host.getAddress();
byte[] portBytes = toByteArray(portNo);
// assemble the PORT command
String cmd = new StringBuffer ("PORT ")
.append (toUnsignedShort (hostBytes[0])) .append (",")
.append (toUnsignedShort (hostBytes[1])) .append (",")
.append (toUnsignedShort (hostBytes[2])) .append (",")
.append (toUnsignedShort (hostBytes[3])) .append (",")
.append (toUnsignedShort (portBytes[0])) .append (",")
.append (toUnsignedShort (portBytes[1])) .toString ();
// send command and check reply
String reply = sendCommand(cmd);
validateReply(reply, "200");
}
/**
* Request a data socket be created on the
* server, connect to it and return our
* connected socket.
*
* @return connected data socket
*/
Socket createDataSocketPASV()
throws IOException, FTPException {
// PASSIVE command - tells the server to listen for
// a connection attempt rather than initiating it
String reply = sendCommand("PASV");
validateReply(reply, "227");
// The reply to PASV is in the form:
// 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2).
// where h1..h4 are the IP address to connect and
// p1,p2 the port number
// Example:
// 227 Entering Passive Mode (128,3,122,1,15,87).
// NOTE: PASV command in IBM/Mainframe returns the string
// 227 Entering Passive Mode 128,3,122,1,15,87 (missing
// brackets)
// extract the IP data string from between the brackets
int startIP = reply.indexOf('(');
int endIP = reply.indexOf(')');
// allow for IBM missing brackets around IP address
if (startIP < 0 && endIP < 0) {
startIP = reply.toUpperCase().lastIndexOf("MODE") + 4;
endIP = reply.length();
}
String ipData = reply.substring(startIP+1,endIP);
int parts[] = new int[6];
int len = ipData.length();
int partCount = 0;
StringBuffer buf = new StringBuffer();
// loop thru and examine each char
for (int i = 0; i < len && partCount <= 6; i++) {
char ch = ipData.charAt(i);
if (Character.isDigit(ch))
buf.append(ch);
else if (ch != ',') {
throw new FTPException("Malformed PASV reply: " + reply);
}
// get the part
if (ch == ',' || i+1 == len) { // at end or at separator
try {
parts[partCount++] = Integer.parseInt(buf.toString());
buf.setLength(0);
}
catch (NumberFormatException ex) {
throw new FTPException("Malformed PASV reply: " + reply);
}
}
}
// assemble the IP address
// we try connecting, so we don't bother checking digits etc
String ipAddress = parts[0] + "."+ parts[1]+ "." +
parts[2] + "." + parts[3];
// assemble the port number
int port = (parts[4] << 8) + parts[5];
// create the socket
return new Socket(ipAddress, port);
}
/**
* Send a command to the FTP server and
* return the server's reply
*
* @return reply to the supplied command
*/
String sendCommand(String command)
throws IOException {
log(DEBUG_ARROW + command);
// send it
writer.write(command + EOL);
writer.flush();
// and read the result
return readReply();
}
/**
* Read the FTP server's reply to a previously
* issued command. RFC 959 states that a reply
* consists of the 3 digit code followed by text.
* The 3 digit code is followed by a hyphen if it
* is a muliline response, and the last line starts
* with the same 3 digit code.
*
* @return reply string
*/
String readReply()
throws IOException {
String firstLine = reader.readLine();
if (firstLine == null || firstLine.length() == 0)
throw new IOException("Unexpected null reply received");
StringBuffer reply = new StringBuffer(firstLine);
log(reply.toString());
String replyCode = reply.toString().substring(0, 3);
// check for multiline response and build up
// the reply
if (reply.charAt(3) == '-') {
boolean complete = false;
while (!complete) {
String line = reader.readLine();
if (line == null)
throw new IOException("Unexpected null reply received");
log(line);
if (line.length() > 3 &&
line.substring(0, 3).equals(replyCode) &&
line.charAt(3) == ' ') {
reply.append(line.substring(3));
complete = true;
}
else { // not the last line
reply.append(" ");
reply.append(line);
}
} // end while
} // end if
return reply.toString();
}
/**
* Validate the response the host has supplied against the
* expected reply. If we get an unexpected reply we throw an
* exception, setting the message to that returned by the
* FTP server
*
* @param reply the entire reply string we received
* @param expectedReplyCode the reply we expected to receive
*
*/
FTPReply validateReply(String reply, String expectedReplyCode)
throws IOException, FTPException {
// all reply codes are 3 chars long
String replyCode = reply.substring(0, 3);
String replyText = reply.substring(4);
FTPReply replyObj = new FTPReply(replyCode, replyText);
if (replyCode.equals(expectedReplyCode))
return replyObj;
// if unexpected reply, throw an exception
throw new FTPException(replyText, replyCode);
}
/**
* Validate the response the host has supplied against the
* expected reply. If we get an unexpected reply we throw an
* exception, setting the message to that returned by the
* FTP server
*
* @param reply the entire reply string we received
* @param expectedReplyCodes array of expected replies
* @return an object encapsulating the server's reply
*
*/
FTPReply validateReply(String reply, String[] expectedReplyCodes)
throws IOException, FTPException {
// all reply codes are 3 chars long
String replyCode = reply.substring(0, 3);
String replyText = reply.substring(4);
FTPReply replyObj = new FTPReply(replyCode, replyText);
for (int i = 0; i < expectedReplyCodes.length; i++)
if (replyCode.equals(expectedReplyCodes[i]))
return replyObj;
// got this far, not recognised
throw new FTPException(replyText, replyCode);
}
/**
* Switch debug of responses on or off
*
* @param on true if you wish to have responses to
* stdout, false otherwise
*/
void debugResponses(boolean on) {
debugResponses = on;
}
/**
* Set the logging stream, replacing
* stdout. If null log supplied, logging is
* switched off
*
* @param log the new logging stream
*/
void setLogStream(PrintWriter log) {
if (log != null)
this.log = log;
else
debugResponses = false;
}
/**
* Log a message, if logging is set up
*
* @param msg message to log
*/
void log(String msg) {
if (debugResponses && log != null) {
if (!msg.startsWith(PASSWORD_MESSAGE))
log.println(msg);
else
log.println(PASSWORD_MESSAGE + " ********");
}
}
}