blob: 825bb13fff7b5a4a1ed1bdd3168e0cba697eac9c [file] [log] [blame]
/*
* Copyright 1994-2008 Sun Microsystems, Inc. All Rights Reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Sun designates this
* particular file as subject to the "Classpath" exception as provided
* by Sun in the LICENSE file that accompanied this code.
*
* This code 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
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
* CA 95054 USA or visit www.sun.com if you need additional information or
* have any questions.
*/
package sun.net.ftp;
import java.util.StringTokenizer;
import java.util.regex.*;
import java.io.*;
import java.net.*;
import sun.net.TransferProtocolClient;
import sun.net.TelnetInputStream;
import sun.net.TelnetOutputStream;
import sun.misc.RegexpPool;
import java.security.AccessController;
import java.security.PrivilegedAction;
/**
* This class implements the FTP client.
*
* @author Jonathan Payne
*/
public class FtpClient extends TransferProtocolClient {
public static final int FTP_PORT = 21;
static int FTP_SUCCESS = 1;
static int FTP_TRY_AGAIN = 2;
static int FTP_ERROR = 3;
/** remember the ftp server name because we may need it */
private String serverName = null;
/** socket for data transfer */
private boolean replyPending = false;
private boolean binaryMode = false;
private boolean loggedIn = false;
/** regexp pool of hosts for which we should connect directly, not Proxy
* these are intialized from a property.
*/
private static RegexpPool nonProxyHostsPool = null;
/** The string soucre of nonProxyHostsPool
*/
private static String nonProxyHostsSource = null;
/** last command issued */
String command;
/** The last reply code from the ftp daemon. */
int lastReplyCode;
/** Welcome message from the server, if any. */
public String welcomeMsg;
/* these methods are used to determine whether ftp urls are sent to */
/* an http server instead of using a direct connection to the */
/* host. They aren't used directly here. */
/**
* @return if the networking layer should send ftp connections through
* a proxy
*/
public static boolean getUseFtpProxy() {
// if the ftp.proxyHost is set, use it!
return (getFtpProxyHost() != null);
}
/**
* @return the host to use, or null if none has been specified
*/
public static String getFtpProxyHost() {
return java.security.AccessController.doPrivileged(
new java.security.PrivilegedAction<String>() {
public String run() {
String result = System.getProperty("ftp.proxyHost");
if (result == null) {
result = System.getProperty("ftpProxyHost");
}
if (result == null) {
// as a last resort we use the general one if ftp.useProxy
// is true
if (Boolean.getBoolean("ftp.useProxy")) {
result = System.getProperty("proxyHost");
}
}
return result;
}
});
}
/**
* @return the proxy port to use. Will default reasonably if not set.
*/
public static int getFtpProxyPort() {
final int result[] = {80};
java.security.AccessController.doPrivileged(
new java.security.PrivilegedAction<Void>() {
public Void run() {
String tmp = System.getProperty("ftp.proxyPort");
if (tmp == null) {
// for compatibility with 1.0.2
tmp = System.getProperty("ftpProxyPort");
}
if (tmp == null) {
// as a last resort we use the general one if ftp.useProxy
// is true
if (Boolean.getBoolean("ftp.useProxy")) {
tmp = System.getProperty("proxyPort");
}
}
if (tmp != null) {
result[0] = Integer.parseInt(tmp);
}
return null;
}
});
return result[0];
}
public static boolean matchNonProxyHosts(String host) {
synchronized (FtpClient.class) {
String rawList = java.security.AccessController.doPrivileged(
new sun.security.action.GetPropertyAction("ftp.nonProxyHosts"));
if (rawList == null) {
nonProxyHostsPool = null;
} else {
if (!rawList.equals(nonProxyHostsSource)) {
RegexpPool pool = new RegexpPool();
StringTokenizer st = new StringTokenizer(rawList, "|", false);
try {
while (st.hasMoreTokens()) {
pool.add(st.nextToken().toLowerCase(), Boolean.TRUE);
}
} catch (sun.misc.REException ex) {
System.err.println("Error in http.nonProxyHosts system property: " + ex);
}
nonProxyHostsPool = pool;
}
}
nonProxyHostsSource = rawList;
}
if (nonProxyHostsPool == null) {
return false;
}
if (nonProxyHostsPool.match(host) != null) {
return true;
} else {
return false;
}
}
/**
* issue the QUIT command to the FTP server and close the connection.
*
* @exception FtpProtocolException if an error occured
*/
public void closeServer() throws IOException {
if (serverIsOpen()) {
issueCommand("QUIT");
super.closeServer();
}
}
/**
* Send a command to the FTP server.
*
* @param cmd String containing the command
* @return reply code
*
* @exception FtpProtocolException if an error occured
*/
protected int issueCommand(String cmd) throws IOException {
command = cmd;
int reply;
while (replyPending) {
replyPending = false;
if (readReply() == FTP_ERROR)
throw new FtpProtocolException("Error reading FTP pending reply\n");
}
do {
sendServer(cmd + "\r\n");
reply = readReply();
} while (reply == FTP_TRY_AGAIN);
return reply;
}
/**
* Send a command to the FTP server and check for success.
*
* @param cmd String containing the command
*
* @exception FtpProtocolException if an error occured
*/
protected void issueCommandCheck(String cmd) throws IOException {
if (issueCommand(cmd) != FTP_SUCCESS)
throw new FtpProtocolException(cmd + ":" + getResponseString());
}
/**
* Read the reply from the FTP server.
*
* @return FTP_SUCCESS or FTP_ERROR depending on success
* @exception FtpProtocolException if an error occured
*/
protected int readReply() throws IOException {
lastReplyCode = readServerResponse();
switch (lastReplyCode / 100) {
case 1:
replyPending = true;
/* falls into ... */
case 2:
case 3:
return FTP_SUCCESS;
case 5:
if (lastReplyCode == 530) {
if (!loggedIn) {
throw new FtpLoginException("Not logged in");
}
return FTP_ERROR;
}
if (lastReplyCode == 550) {
throw new FileNotFoundException(command + ": " + getResponseString());
}
}
/* this statement is not reached */
return FTP_ERROR;
}
/**
* Tries to open a Data Connection in "PASSIVE" mode by issuing a EPSV or
* PASV command then opening a Socket to the specified address & port
*
* @return the opened socket
* @exception FtpProtocolException if an error occurs when issuing the
* PASV command to the ftp server.
*/
protected Socket openPassiveDataConnection() throws IOException {
String serverAnswer;
int port;
InetSocketAddress dest = null;
/**
* Here is the idea:
*
* - First we want to try the new (and IPv6 compatible) EPSV command
* But since we want to be nice with NAT software, we'll issue the
* EPSV ALL cmd first.
* EPSV is documented in RFC2428
* - If EPSV fails, then we fall back to the older, yet OK PASV command
* - If PASV fails as well, then we throw an exception and the calling method
* will have to try the EPRT or PORT command
*/
if (issueCommand("EPSV ALL") == FTP_SUCCESS) {
// We can safely use EPSV commands
if (issueCommand("EPSV") == FTP_ERROR)
throw new FtpProtocolException("EPSV Failed: " + getResponseString());
serverAnswer = getResponseString();
// The response string from a EPSV command will contain the port number
// the format will be :
// 229 Entering Extended Passive Mode (|||58210|)
//
// So we'll use the regular expresions package to parse the output.
Pattern p = Pattern.compile("^229 .* \\(\\|\\|\\|(\\d+)\\|\\)");
Matcher m = p.matcher(serverAnswer);
if (! m.find())
throw new FtpProtocolException("EPSV failed : " + serverAnswer);
// Yay! Let's extract the port number
String s = m.group(1);
port = Integer.parseInt(s);
InetAddress add = serverSocket.getInetAddress();
if (add != null) {
dest = new InetSocketAddress(add, port);
} else {
// This means we used an Unresolved address to connect in
// the first place. Most likely because the proxy is doing
// the name resolution for us, so let's keep using unresolved
// address.
dest = InetSocketAddress.createUnresolved(serverName, port);
}
} else {
// EPSV ALL failed, so Let's try the regular PASV cmd
if (issueCommand("PASV") == FTP_ERROR)
throw new FtpProtocolException("PASV failed: " + getResponseString());
serverAnswer = getResponseString();
// Let's parse the response String to get the IP & port to connect to
// the String should be in the following format :
//
// 227 Entering Passive Mode (A1,A2,A3,A4,p1,p2)
//
// Note that the two parenthesis are optional
//
// The IP address is A1.A2.A3.A4 and the port is p1 * 256 + p2
//
// The regular expression is a bit more complex this time, because the
// parenthesis are optionals and we have to use 3 groups.
Pattern p = Pattern.compile("227 .* \\(?(\\d{1,3},\\d{1,3},\\d{1,3},\\d{1,3}),(\\d{1,3}),(\\d{1,3})\\)?");
Matcher m = p.matcher(serverAnswer);
if (! m.find())
throw new FtpProtocolException("PASV failed : " + serverAnswer);
// Get port number out of group 2 & 3
port = Integer.parseInt(m.group(3)) + (Integer.parseInt(m.group(2)) << 8);
// IP address is simple
String s = m.group(1).replace(',','.');
dest = new InetSocketAddress(s, port);
}
// Got everything, let's open the socket!
Socket s;
if (proxy != null) {
if (proxy.type() == Proxy.Type.SOCKS) {
s = AccessController.doPrivileged(
new PrivilegedAction<Socket>() {
public Socket run() {
return new Socket(proxy);
}});
} else
s = new Socket(Proxy.NO_PROXY);
} else
s = new Socket();
if (connectTimeout >= 0) {
s.connect(dest, connectTimeout);
} else {
if (defaultConnectTimeout > 0) {
s.connect(dest, defaultConnectTimeout);
} else {
s.connect(dest);
}
}
if (readTimeout >= 0)
s.setSoTimeout(readTimeout);
else
if (defaultSoTimeout > 0) {
s.setSoTimeout(defaultSoTimeout);
}
return s;
}
/**
* Tries to open a Data Connection with the server. It will first try a passive
* mode connection, then, if it fails, a more traditional PORT command
*
* @param cmd the command to execute (RETR, STOR, etc...)
* @return the opened socket
*
* @exception FtpProtocolException if an error occurs when issuing the
* PORT command to the ftp server.
*/
protected Socket openDataConnection(String cmd) throws IOException {
ServerSocket portSocket;
Socket clientSocket = null;
String portCmd;
InetAddress myAddress;
IOException e;
// Let's try passive mode first
try {
clientSocket = openPassiveDataConnection();
} catch (IOException ex) {
clientSocket = null;
}
if (clientSocket != null) {
// We did get a clientSocket, so the passive mode worked
// Let's issue the command (GET, DIR, ...)
try {
if (issueCommand(cmd) == FTP_ERROR) {
clientSocket.close();
throw new FtpProtocolException(getResponseString());
} else
return clientSocket;
} catch (IOException ioe) {
clientSocket.close();
throw ioe;
}
}
assert(clientSocket == null);
// Passive mode failed, let's fall back to the good old "PORT"
if (proxy != null && proxy.type() == Proxy.Type.SOCKS) {
// We're behind a firewall and the passive mode fail,
// since we can't accept a connection through SOCKS (yet)
// throw an exception
throw new FtpProtocolException("Passive mode failed");
} else
portSocket = new ServerSocket(0, 1);
try {
myAddress = portSocket.getInetAddress();
if (myAddress.isAnyLocalAddress())
myAddress = getLocalAddress();
// Let's try the new, IPv6 compatible EPRT command
// See RFC2428 for specifics
// Some FTP servers (like the one on Solaris) are bugged, they
// will accept the EPRT command but then, the subsequent command
// (e.g. RETR) will fail, so we have to check BOTH results (the
// EPRT cmd then the actual command) to decide wether we should
// fall back on the older PORT command.
portCmd = "EPRT |" +
((myAddress instanceof Inet6Address) ? "2" : "1") + "|" +
myAddress.getHostAddress() +"|" +
portSocket.getLocalPort()+"|";
if (issueCommand(portCmd) == FTP_ERROR ||
issueCommand(cmd) == FTP_ERROR) {
// The EPRT command failed, let's fall back to good old PORT
portCmd = "PORT ";
byte[] addr = myAddress.getAddress();
/* append host addr */
for (int i = 0; i < addr.length; i++) {
portCmd = portCmd + (addr[i] & 0xFF) + ",";
}
/* append port number */
portCmd = portCmd + ((portSocket.getLocalPort() >>> 8) & 0xff) + ","
+ (portSocket.getLocalPort() & 0xff);
if (issueCommand(portCmd) == FTP_ERROR) {
e = new FtpProtocolException("PORT :" + getResponseString());
throw e;
}
if (issueCommand(cmd) == FTP_ERROR) {
e = new FtpProtocolException(cmd + ":" + getResponseString());
throw e;
}
}
// Either the EPRT or the PORT command was successful
// Let's create the client socket
if (connectTimeout >= 0) {
portSocket.setSoTimeout(connectTimeout);
} else {
if (defaultConnectTimeout > 0)
portSocket.setSoTimeout(defaultConnectTimeout);
}
clientSocket = portSocket.accept();
if (readTimeout >= 0)
clientSocket.setSoTimeout(readTimeout);
else {
if (defaultSoTimeout > 0)
clientSocket.setSoTimeout(defaultSoTimeout);
}
} finally {
portSocket.close();
}
return clientSocket;
}
/* public methods */
/**
* Open a FTP connection to host <i>host</i>.
*
* @param host The hostname of the ftp server
*
* @exception FtpProtocolException if connection fails
*/
public void openServer(String host) throws IOException {
openServer(host, FTP_PORT);
}
/**
* Open a FTP connection to host <i>host</i> on port <i>port</i>.
*
* @param host the hostname of the ftp server
* @param port the port to connect to (usually 21)
*
* @exception FtpProtocolException if connection fails
*/
public void openServer(String host, int port) throws IOException {
this.serverName = host;
super.openServer(host, port);
if (readReply() == FTP_ERROR)
throw new FtpProtocolException("Welcome message: " +
getResponseString());
}
/**
* login user to a host with username <i>user</i> and password
* <i>password</i>
*
* @param user Username to use at login
* @param password Password to use at login or null of none is needed
*
* @exception FtpLoginException if login is unsuccesful
*/
public void login(String user, String password) throws IOException {
if (!serverIsOpen())
throw new FtpLoginException("not connected to host");
if (user == null || user.length() == 0)
return;
if (issueCommand("USER " + user) == FTP_ERROR)
throw new FtpLoginException("user " + user + " : " + getResponseString());
/*
* Checks for "331 User name okay, need password." answer
*/
if (lastReplyCode == 331)
if ((password == null) || (password.length() == 0) ||
(issueCommand("PASS " + password) == FTP_ERROR))
throw new FtpLoginException("password: " + getResponseString());
// keep the welcome message around so we can
// put it in the resulting HTML page.
String l;
StringBuffer sb = new StringBuffer();
for (int i = 0; i < serverResponse.size(); i++) {
l = (String)serverResponse.elementAt(i);
if (l != null) {
if (l.length() >= 4 && l.startsWith("230")) {
// get rid of the "230-" prefix
l = l.substring(4);
}
sb.append(l);
}
}
welcomeMsg = sb.toString();
loggedIn = true;
}
/**
* GET a file from the FTP server
*
* @param filename name of the file to retrieve
* @return the <code>InputStream</code> to read the file from
*
* @exception FileNotFoundException if the file can't be opened
*/
public TelnetInputStream get(String filename) throws IOException {
Socket s;
try {
s = openDataConnection("RETR " + filename);
} catch (FileNotFoundException fileException) {
/* Well, "/" might not be the file delimitor for this
particular ftp server, so let's try a series of
"cd" commands to get to the right place. */
/* But don't try this if there are no '/' in the path */
if (filename.indexOf('/') == -1)
throw fileException;
StringTokenizer t = new StringTokenizer(filename, "/");
String pathElement = null;
while (t.hasMoreElements()) {
pathElement = t.nextToken();
if (!t.hasMoreElements()) {
/* This is the file component. Look it up now. */
break;
}
try {
cd(pathElement);
} catch (FtpProtocolException e) {
/* Giving up. */
throw fileException;
}
}
if (pathElement != null) {
s = openDataConnection("RETR " + pathElement);
} else {
throw fileException;
}
}
return new TelnetInputStream(s.getInputStream(), binaryMode);
}
/**
* PUT a file to the FTP server
*
* @param filename name of the file to store
* @return the <code>OutputStream</code> to write the file to
*
*/
public TelnetOutputStream put(String filename) throws IOException {
Socket s = openDataConnection("STOR " + filename);
TelnetOutputStream out = new TelnetOutputStream(s.getOutputStream(), binaryMode);
if (!binaryMode)
out.setStickyCRLF(true);
return out;
}
/**
* Append to a file on the FTP server
*
* @param filename name of the file to append to
* @return the <code>OutputStream</code> to write the file to
*
*/
public TelnetOutputStream append(String filename) throws IOException {
Socket s = openDataConnection("APPE " + filename);
TelnetOutputStream out = new TelnetOutputStream(s.getOutputStream(), binaryMode);
if (!binaryMode)
out.setStickyCRLF(true);
return out;
}
/**
* LIST files in the current directory on a remote FTP server
*
* @return the <code>InputStream</code> to read the list from
*
*/
public TelnetInputStream list() throws IOException {
Socket s = openDataConnection("LIST");
return new TelnetInputStream(s.getInputStream(), binaryMode);
}
/**
* List (NLST) file names on a remote FTP server
*
* @param path pathname to the directory to list, null for current
* directory
* @return the <code>InputStream</code> to read the list from
* @exception <code>FtpProtocolException</code>
*/
public TelnetInputStream nameList(String path) throws IOException {
Socket s;
if (path != null)
s = openDataConnection("NLST " + path);
else
s = openDataConnection("NLST");
return new TelnetInputStream(s.getInputStream(), binaryMode);
}
/**
* CD to a specific directory on a remote FTP server
*
* @param remoteDirectory path of the directory to CD to
*
* @exception <code>FtpProtocolException</code>
*/
public void cd(String remoteDirectory) throws IOException {
if (remoteDirectory == null ||
"".equals(remoteDirectory))
return;
issueCommandCheck("CWD " + remoteDirectory);
}
/**
* CD to the parent directory on a remote FTP server
*
*/
public void cdUp() throws IOException {
issueCommandCheck("CDUP");
}
/**
* Print working directory of remote FTP server
*
* @exception FtpProtocolException if the command fails
*/
public String pwd() throws IOException {
String answ;
issueCommandCheck("PWD");
/*
* answer will be of the following format :
*
* 257 "/" is current directory.
*/
answ = getResponseString();
if (!answ.startsWith("257"))
throw new FtpProtocolException("PWD failed. " + answ);
return answ.substring(5, answ.lastIndexOf('"'));
}
/**
* Set transfer type to 'I'
*
* @exception FtpProtocolException if the command fails
*/
public void binary() throws IOException {
issueCommandCheck("TYPE I");
binaryMode = true;
}
/**
* Set transfer type to 'A'
*
* @exception FtpProtocolException if the command fails
*/
public void ascii() throws IOException {
issueCommandCheck("TYPE A");
binaryMode = false;
}
/**
* Rename a file on the ftp server
*
* @exception FtpProtocolException if the command fails
*/
public void rename(String from, String to) throws IOException {
issueCommandCheck("RNFR " + from);
issueCommandCheck("RNTO " + to);
}
/**
* Get the "System string" from the FTP server
*
* @exception FtpProtocolException if it fails
*/
public String system() throws IOException {
String answ;
issueCommandCheck("SYST");
answ = getResponseString();
if (!answ.startsWith("215"))
throw new FtpProtocolException("SYST failed." + answ);
return answ.substring(4); // Skip "215 "
}
/**
* Send a No-operation command. It's usefull for testing the connection status
*
* @exception FtpProtocolException if the command fails
*/
public void noop() throws IOException {
issueCommandCheck("NOOP");
}
/**
* Reinitialize the USER parameters on the FTp server
*
* @exception FtpProtocolException if the command fails
*/
public void reInit() throws IOException {
issueCommandCheck("REIN");
loggedIn = false;
}
/**
* New FTP client connected to host <i>host</i>.
*
* @param host Hostname of the FTP server
*
* @exception FtpProtocolException if the connection fails
*/
public FtpClient(String host) throws IOException {
super();
openServer(host, FTP_PORT);
}
/**
* New FTP client connected to host <i>host</i>, port <i>port</i>.
*
* @param host Hostname of the FTP server
* @param port port number to connect to (usually 21)
*
* @exception FtpProtocolException if the connection fails
*/
public FtpClient(String host, int port) throws IOException {
super();
openServer(host, port);
}
/** Create an uninitialized FTP client. */
public FtpClient() {}
public FtpClient(Proxy p) {
proxy = p;
}
protected void finalize() throws IOException {
/**
* Do not call the "normal" closeServer() as we want finalization
* to be as efficient as possible
*/
if (serverIsOpen())
super.closeServer();
}
}