/* * Copyright (c) 1994, 2004, Oracle and/or its affiliates. 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. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle 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 Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.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() { public Object 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 = (Socket) AccessController.doPrivileged( new PrivilegedAction() { public Object 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(); } }