From aa9d763f67d88f3e20026f450b792d0adc941872 Mon Sep 17 00:00:00 2001 From: jccollet Date: Wed, 21 Oct 2009 16:28:57 +0200 Subject: [PATCH] 6893702: Overhaul of Ftp Client internal code Summary: Major reorg of internal FTP client code Reviewed-by: chegar --- jdk/make/sun/net/FILES_java.gmk | 6 + .../share/classes/sun/net/ftp/FtpClient.java | 1420 ++++++----- .../sun/net/ftp/FtpClientProvider.java | 158 ++ .../classes/sun/net/ftp/FtpDirEntry.java | 331 +++ .../classes/sun/net/ftp/FtpDirParser.java | 49 + .../sun/net/ftp/FtpLoginException.java | 8 +- .../sun/net/ftp/FtpProtocolException.java | 48 +- .../classes/sun/net/ftp/FtpReplyCode.java | 248 ++ .../ftp/impl/DefaultFtpClientProvider.java | 38 + .../classes/sun/net/ftp/impl/FtpClient.java | 2191 +++++++++++++++++ .../www/protocol/ftp/FtpURLConnection.java | 319 ++- 11 files changed, 4020 insertions(+), 796 deletions(-) create mode 100644 jdk/src/share/classes/sun/net/ftp/FtpClientProvider.java create mode 100644 jdk/src/share/classes/sun/net/ftp/FtpDirEntry.java create mode 100644 jdk/src/share/classes/sun/net/ftp/FtpDirParser.java create mode 100644 jdk/src/share/classes/sun/net/ftp/FtpReplyCode.java create mode 100644 jdk/src/share/classes/sun/net/ftp/impl/DefaultFtpClientProvider.java create mode 100644 jdk/src/share/classes/sun/net/ftp/impl/FtpClient.java diff --git a/jdk/make/sun/net/FILES_java.gmk b/jdk/make/sun/net/FILES_java.gmk index 19dd2d6d7f..4f2a5165a7 100644 --- a/jdk/make/sun/net/FILES_java.gmk +++ b/jdk/make/sun/net/FILES_java.gmk @@ -45,8 +45,14 @@ FILES_java = \ sun/net/dns/ResolverConfiguration.java \ sun/net/dns/ResolverConfigurationImpl.java \ sun/net/ftp/FtpClient.java \ + sun/net/ftp/FtpClientProvider.java \ + sun/net/ftp/FtpDirEntry.java \ + sun/net/ftp/FtpReplyCode.java \ + sun/net/ftp/FtpDirParser.java \ sun/net/ftp/FtpLoginException.java \ sun/net/ftp/FtpProtocolException.java \ + sun/net/ftp/impl/FtpClient.java \ + sun/net/ftp/impl/DefaultFtpClientProvider.java \ sun/net/spi/DefaultProxySelector.java \ sun/net/spi/nameservice/NameServiceDescriptor.java \ sun/net/spi/nameservice/NameService.java \ diff --git a/jdk/src/share/classes/sun/net/ftp/FtpClient.java b/jdk/src/share/classes/sun/net/ftp/FtpClient.java index 2cd51192ce..35e6b6561e 100644 --- a/jdk/src/share/classes/sun/net/ftp/FtpClient.java +++ b/jdk/src/share/classes/sun/net/ftp/FtpClient.java @@ -1,5 +1,5 @@ /* - * Copyright 1994-2008 Sun Microsystems, Inc. All Rights Reserved. + * Copyright 2009 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 @@ -22,794 +22,922 @@ * 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; +import java.io.*; +import java.util.Date; +import java.util.List; +import java.util.Iterator; /** - * This class implements the FTP client. + * A class that implements the FTP protocol according to + * RFCs 959, + * 2228, + * 2389, + * 2428, + * 3659, + * 4217. + * Which includes support for FTP over SSL/TLS (aka ftps). * - * @author Jonathan Payne + * {@code FtpClient} provides all the functionalities of a typical FTP + * client, like storing or retrieving files, listing or creating directories. + * A typical usage would consist of connecting the client to the server, + * log in, issue a few commands then logout. + * Here is a code example: + *
+ * FtpClient cl = FtpClient.create();
+ * cl.connect("ftp.gnu.org").login("anonymous", "john.doe@mydomain.com".toCharArray())).changeDirectory("pub/gnu");
+ * Iterator<FtpDirEntry> dir = cl.listFiles();
+ *     while (dir.hasNext()) {
+ *         FtpDirEntry f = dir.next();
+ *         System.err.println(f.getName());
+ *     }
+ *     cl.close();
+ * }
+ * 
+ *

Error reporting: There are, mostly, two families of errors that + * can occur during an FTP session. The first kind are the network related issues + * like a connection reset, and they are usually fatal to the session, meaning, + * in all likelyhood the connection to the server has been lost and the session + * should be restarted from scratch. These errors are reported by throwing an + * {@link IOException}. The second kind are the errors reported by the FTP server, + * like when trying to download a non-existing file for example. These errors + * are usually non fatal to the session, meaning more commands can be sent to the + * server. In these cases, a {@link FtpProtocolException} is thrown.

+ *

+ * It should be noted that this is not a thread-safe API, as it wouldn't make + * too much sense, due to the very sequential nature of FTP, to provide a + * client able to be manipulated from multiple threads. + * + * @since 1.7 */ +public abstract class FtpClient implements java.io.Closeable { -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; + private static final int FTP_PORT = 21; - /** remember the ftp server name because we may need it */ - private String serverName = null; + public static enum TransferType { - /** socket for data transfer */ - private boolean replyPending = false; - private boolean binaryMode = false; - private boolean loggedIn = false; + ASCII, BINARY, EBCDIC + }; - /** regexp pool of hosts for which we should connect directly, not Proxy - * these are intialized from a property. + /** + * Returns the default FTP port number. + * + * @return the port number. */ - private static RegexpPool nonProxyHostsPool = null; + public static final int defaultPort() { + return FTP_PORT; + } - /** The string soucre of nonProxyHostsPool + /** + * Creates an instance of FtpClient. The client is not connected to any + * server yet. + * */ - 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; - + protected FtpClient() { + } - /* 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 + * Creates an instance of {@code FtpClient}. The client is not connected to any + * server yet. + * + * @return the created {@code FtpClient} */ - public static boolean getUseFtpProxy() { - // if the ftp.proxyHost is set, use it! - return (getFtpProxyHost() != null); + public static FtpClient create() { + FtpClientProvider provider = FtpClientProvider.provider(); + return provider.createFtpClient(); } /** - * @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() { - 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; - } - }); + * Creates an instance of FtpClient and connects it to the specified + * address. + * + * @param dest the {@code InetSocketAddress} to connect to. + * @return The created {@code FtpClient} + * @throws IOException if the connection fails + * @see #connect(java.net.SocketAddress) + */ + public static FtpClient create(InetSocketAddress dest) throws FtpProtocolException, IOException { + FtpClient client = create(); + if (dest != null) { + client.connect(dest); + } + return client; } /** - * @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 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]; + * Creates an instance of {@code FtpClient} and connects it to the + * specified host on the default FTP port. + * + * @param dest the {@code String} containing the name of the host + * to connect to. + * @return The created {@code FtpClient} + * @throws IOException if the connection fails. + * @throws FtpProtocolException if the server rejected the connection + */ + public static FtpClient create(String dest) throws FtpProtocolException, IOException { + return create(new InetSocketAddress(dest, FTP_PORT)); } - 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; - } - } + /** + * Enables, or disables, the use of the passive mode. In that mode, + * data connections are established by having the client connect to the server. + * This is the recommended default mode as it will work best through + * firewalls and NATs. If set to {@code false} the mode is said to be + * active which means the server will connect back to the client + * after a PORT command to establish a data connection. + * + *

Note: Since the passive mode might not be supported by all + * FTP servers, enabling it means the client will try to use it. If the + * server rejects it, then the client will attempt to fall back to using + * the active mode by issuing a {@code PORT} command instead.

+ * + * @param passive {@code true} to force passive mode. + * @return This FtpClient + * @see #isPassiveModeEnabled() + */ + public abstract FtpClient enablePassiveMode(boolean passive); /** - * issue the QUIT command to the FTP server and close the connection. + * Tests whether passive mode is enabled. * - * @exception FtpProtocolException if an error occured + * @return {@code true} if the passive mode has been enabled. + * @see #enablePassiveMode(boolean) */ - public void closeServer() throws IOException { - if (serverIsOpen()) { - issueCommand("QUIT"); - super.closeServer(); - } - } + public abstract boolean isPassiveModeEnabled(); /** - * Send a command to the FTP server. + * Sets the default timeout value to use when connecting to the server, * - * @param cmd String containing the command - * @return reply code + * @param timeout the timeout value, in milliseconds, to use for the connect + * operation. A value of zero or less, means use the default timeout. * - * @exception FtpProtocolException if an error occured + * @return This FtpClient */ - 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; - } + public abstract FtpClient setConnectTimeout(int timeout); /** - * Send a command to the FTP server and check for success. + * Returns the current default connection timeout value. * - * @param cmd String containing the command + * @return the value, in milliseconds, of the current connect timeout. + * @see #setConnectTimeout(int) + */ + public abstract int getConnectTimeout(); + + /** + * Sets the timeout value to use when reading from the server, * - * @exception FtpProtocolException if an error occured + * @param timeout the timeout value, in milliseconds, to use for the read + * operation. A value of zero or less, means use the default timeout. + * @return This FtpClient */ - protected void issueCommandCheck(String cmd) throws IOException { - if (issueCommand(cmd) != FTP_SUCCESS) - throw new FtpProtocolException(cmd + ":" + getResponseString()); - } + public abstract FtpClient setReadTimeout(int timeout); /** - * Read the reply from the FTP server. + * Returns the current read timeout value. * - * @return FTP_SUCCESS or FTP_ERROR depending on success - * @exception FtpProtocolException if an error occured + * @return the value, in milliseconds, of the current read timeout. + * @see #setReadTimeout(int) */ - protected int readReply() throws IOException { - lastReplyCode = readServerResponse(); + public abstract int getReadTimeout(); - switch (lastReplyCode / 100) { - case 1: - replyPending = true; - /* falls into ... */ + /** + * Set the {@code Proxy} to be used for the next connection. + * If the client is already connected, it doesn't affect the current + * connection. However it is not recommended to change this during a session. + * + * @param p the {@code Proxy} to use, or {@code null} for no proxy. + * @return This FtpClient + */ + public abstract FtpClient setProxy(Proxy p); - case 2: - case 3: - return FTP_SUCCESS; + /** + * Get the proxy of this FtpClient + * + * @return the {@code Proxy}, this client is using, or {@code null} + * if none is used. + * @see #setProxy(Proxy) + */ + public abstract Proxy getProxy(); - case 5: - if (lastReplyCode == 530) { - if (!loggedIn) { - throw new FtpLoginException("Not logged in"); - } - return FTP_ERROR; - } - if (lastReplyCode == 550) { - throw new FileNotFoundException(command + ": " + getResponseString()); - } - } + /** + * Tests whether this client is connected or not to a server. + * + * @return {@code true} if the client is connected. + */ + public abstract boolean isConnected(); - /* this statement is not reached */ - return FTP_ERROR; - } + /** + * Connects the {@code FtpClient} to the specified destination server. + * + * @param dest the address of the destination server + * @return this FtpClient + * @throws IOException if connection failed. + * @throws SecurityException if there is a SecurityManager installed and it + * denied the authorization to connect to the destination. + * @throws FtpProtocolException + */ + public abstract FtpClient connect(SocketAddress dest) throws FtpProtocolException, IOException; /** - * 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() { - public Socket run() { - return new Socket(proxy); - }}); - } else - s = new Socket(Proxy.NO_PROXY); - } else - s = new Socket(); - // Bind the socket to the same address as the control channel. This - // is needed in case of multi-homed systems. - s.bind(new InetSocketAddress(serverSocket.getLocalAddress(),0)); - 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; - } + * Connects the FtpClient to the specified destination server. + * + * @param dest the address of the destination server + * @param timeout the value, in milliseconds, to use as a connection timeout + * @return this FtpClient + * @throws IOException if connection failed. + * @throws SecurityException if there is a SecurityManager installed and it + * denied the authorization to connect to the destination. + * @throws FtpProtocolException + */ + public abstract FtpClient connect(SocketAddress dest, int timeout) throws FtpProtocolException, IOException; /** - * 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 + * Retrieves the address of the FTP server this client is connected to. * - * @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. + * @return the {@link SocketAddress} of the server, or {@code null} if this + * client is not connected yet. */ - 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; - } - } + public abstract SocketAddress getServerAddress(); - assert(clientSocket == null); + /** + * Attempts to log on the server with the specified user name and password. + * + * @param user The user name + * @param password The password for that user + * @return this FtpClient + * @throws IOException if an error occured during the transmission + * @throws FtpProtocolException if the login was refused by the server + */ + public abstract FtpClient login(String user, char[] password) throws FtpProtocolException, IOException; - // Passive mode failed, let's fall back to the good old "PORT" + /** + * Attempts to log on the server with the specified user name, password and + * account name. + * + * @param user The user name + * @param password The password for that user. + * @param account The account name for that user. + * @return this FtpClient + * @throws IOException if an error occurs during the transmission. + * @throws FtpProtocolException if the login was refused by the server + */ + public abstract FtpClient login(String user, char[] password, String account) throws FtpProtocolException, IOException; - 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"); - } - // Bind the ServerSocket to the same address as the control channel - // This is needed for multi-homed systems - portSocket = new ServerSocket(0, 1, serverSocket.getLocalAddress()); - 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(); - } + /** + * Closes the current connection. Logs out the current user, if any, by + * issuing the QUIT command to the server. + * This is in effect terminates the current + * session and the connection to the server will be closed. + *

After a close, the client can then be connected to another server + * to start an entirely different session.

+ * + * @throws IOException if an error occurs during transmission + */ + public abstract void close() throws IOException; - return clientSocket; - } + /** + * Checks whether the client is logged in to the server or not. + * + * @return {@code true} if the client has already completed a login. + */ + public abstract boolean isLoggedIn(); - /* public methods */ + /** + * Changes to a specific directory on a remote FTP server + * + * @param remoteDirectory path of the directory to CD to. + * @return this FtpClient + * @throws IOException if an error occurs during the transmission. + * @throws FtpProtocolException if the command was refused by the server + */ + public abstract FtpClient changeDirectory(String remoteDirectory) throws FtpProtocolException, IOException; /** - * Open a FTP connection to host host. + * Changes to the parent directory, sending the CDUP command to the server. * - * @param host The hostname of the ftp server + * @return this FtpClient + * @throws IOException if an error occurs during the transmission. + * @throws FtpProtocolException if the command was refused by the server + */ + public abstract FtpClient changeToParentDirectory() throws FtpProtocolException, IOException; + + /** + * Retrieve the server current working directory using the PWD command. * - * @exception FtpProtocolException if connection fails + * @return a {@code String} containing the current working directory + * @throws IOException if an error occurs during transmission + * @throws FtpProtocolException if the command was refused by the server, */ - public void openServer(String host) throws IOException { - openServer(host, FTP_PORT); - } + public abstract String getWorkingDirectory() throws FtpProtocolException, IOException; /** - * Open a FTP connection to host host on port port. + * Sets the restart offset to the specified value. That value will be + * sent through a {@code REST} command to server before the next file + * transfer and has the effect of resuming a file transfer from the + * specified point. After the transfer the restart offset is set back to + * zero. + * + * @param offset the offset in the remote file at which to start the next + * transfer. This must be a value greater than or equal to zero. + * @return this FtpClient + * @throws IllegalArgumentException if the offset is negative. + */ + public abstract FtpClient setRestartOffset(long offset); + + /** + * Retrieves a file from the ftp server and writes its content to the specified + * {@code OutputStream}. + *

If the restart offset was set, then a {@code REST} command will be + * sent before the {@code RETR} in order to restart the tranfer from the specified + * offset.

+ *

The {@code OutputStream} is not closed by this method at the end + * of the transfer.

+ *

This method will block until the transfer is complete or an exception + * is thrown.

+ * + * @param name a {@code String} containing the name of the file to + * retreive from the server. + * @param local the {@code OutputStream} the file should be written to. + * @return this FtpClient + * @throws IOException if the transfer fails. + * @throws FtpProtocolException if the command was refused by the server + * @see #setRestartOffset(long) + */ + public abstract FtpClient getFile(String name, OutputStream local) throws FtpProtocolException, IOException; + + /** + * Retrieves a file from the ftp server, using the {@code RETR} command, and + * returns the InputStream from the established data connection. + * {@link #completePending()} has to be called once the application + * is done reading from the returned stream. + *

If the restart offset was set, then a {@code REST} command will be + * sent before the {@code RETR} in order to restart the tranfer from the specified + * offset.

+ * + * @param name the name of the remote file + * @return the {@link java.io.InputStream} from the data connection + * @throws IOException if an error occured during the transmission. + * @throws FtpProtocolException if the command was refused by the server + * @see #setRestartOffset(long) + */ + public abstract InputStream getFileStream(String name) throws FtpProtocolException, IOException; + + /** + * Transfers a file from the client to the server (aka a put) + * by sending the STOR command, and returns the {@code OutputStream} + * from the established data connection. + * + * A new file is created at the server site if the file specified does + * not already exist. * - * @param host the hostname of the ftp server - * @param port the port to connect to (usually 21) + * {@link #completePending()} has to be called once the application + * is finished writing to the returned stream. * - * @exception FtpProtocolException if connection fails + * @param name the name of the remote file to write. + * @return the {@link java.io.OutputStream} from the data connection or + * {@code null} if the command was unsuccessful. + * @throws IOException if an error occured during the transmission. + * @throws FtpProtocolException if the command was rejected by the server */ - 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()); + public OutputStream putFileStream(String name) throws FtpProtocolException, IOException { + return putFileStream(name, false); } + /** + * Transfers a file from the client to the server (aka a put) + * by sending the STOR or STOU command, depending on the + * {@code unique} argument, and returns the {@code OutputStream} + * from the established data connection. + * {@link #completePending()} has to be called once the application + * is finished writing to the stream. + * + * A new file is created at the server site if the file specified does + * not already exist. + * + * If {@code unique} is set to {@code true}, the resultant file + * is to be created under a name unique to that directory, meaning + * it will not overwrite an existing file, instead the server will + * generate a new, unique, file name. + * The name of the remote file can be retrieved, after completion of the + * transfer, by calling {@link #getLastFileName()}. + * + * @param name the name of the remote file to write. + * @param unique {@code true} if the remote files should be unique, + * in which case the STOU command will be used. + * @return the {@link java.io.OutputStream} from the data connection. + * @throws IOException if an error occured during the transmission. + * @throws FtpProtocolException if the command was rejected by the server + */ + public abstract OutputStream putFileStream(String name, boolean unique) throws FtpProtocolException, IOException; /** - * login user to a host with username user and password - * password - * - * @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; + * Transfers a file from the client to the server (aka a put) + * by sending the STOR or STOU command, depending on the + * {@code unique} argument. The content of the {@code InputStream} + * passed in argument is written into the remote file, overwriting any + * existing data. + * + * A new file is created at the server site if the file specified does + * not already exist. + * + * If {@code unique} is set to {@code true}, the resultant file + * is to be created under a name unique to that directory, meaning + * it will not overwrite an existing file, instead the server will + * generate a new, unique, file name. + * The name of the remote file can be retrieved, after completion of the + * transfer, by calling {@link #getLastFileName()}. + * + *

This method will block until the transfer is complete or an exception + * is thrown.

+ * + * @param name the name of the remote file to write. + * @param local the {@code InputStream} that points to the data to + * transfer. + * @return this FtpClient + * @throws IOException if an error occured during the transmission. + * @throws FtpProtocolException if the command was rejected by the server + */ + public FtpClient putFile(String name, InputStream local) throws FtpProtocolException, IOException { + return putFile(name, local, false); } /** - * GET a file from the FTP server - * - * @param filename name of the file to retrieve - * @return the InputStream 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; - } - } + * Transfers a file from the client to the server (aka a put) + * by sending the STOR command. The content of the {@code InputStream} + * passed in argument is written into the remote file, overwriting any + * existing data. + * + * A new file is created at the server site if the file specified does + * not already exist. + * + *

This method will block until the transfer is complete or an exception + * is thrown.

+ * + * @param name the name of the remote file to write. + * @param local the {@code InputStream} that points to the data to + * transfer. + * @param unique {@code true} if the remote file should be unique + * (i.e. not already existing), {@code false} otherwise. + * @return this FtpClient + * @throws IOException if an error occured during the transmission. + * @throws FtpProtocolException if the command was rejected by the server + * @see #getLastFileName() + */ + public abstract FtpClient putFile(String name, InputStream local, boolean unique) throws FtpProtocolException, IOException; - return new TelnetInputStream(s.getInputStream(), binaryMode); - } + /** + * Sends the APPE command to the server in order to transfer a data stream + * passed in argument and append it to the content of the specified remote + * file. + * + *

This method will block until the transfer is complete or an exception + * is thrown.

+ * + * @param name A {@code String} containing the name of the remote file + * to append to. + * @param local The {@code InputStream} providing access to the data + * to be appended. + * @return this FtpClient + * @throws IOException if an error occured during the transmission. + * @throws FtpProtocolException if the command was rejected by the server + */ + public abstract FtpClient appendFile(String name, InputStream local) throws FtpProtocolException, IOException; /** - * PUT a file to the FTP server - * - * @param filename name of the file to store - * @return the OutputStream to write the file to + * Renames a file on the server. * + * @param from the name of the file being renamed + * @param to the new name for the file + * @return this FtpClient + * @throws IOException if an error occured during the transmission. + * @throws FtpProtocolException if the command was rejected by the server */ - 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; - } + public abstract FtpClient rename(String from, String to) throws FtpProtocolException, IOException; /** - * Append to a file on the FTP server - * - * @param filename name of the file to append to - * @return the OutputStream to write the file to + * Deletes a file on the server. * + * @param name a {@code String} containing the name of the file + * to delete. + * @return this FtpClient + * @throws IOException if an error occured during the exchange + * @throws FtpProtocolException if the command was rejected by the server */ - public TelnetOutputStream append(String filename) throws IOException { - Socket s = openDataConnection("APPE " + filename); - TelnetOutputStream out = new TelnetOutputStream(s.getOutputStream(), binaryMode); - if (!binaryMode) - out.setStickyCRLF(true); + public abstract FtpClient deleteFile(String name) throws FtpProtocolException, IOException; - return out; - } + /** + * Creates a new directory on the server. + * + * @param name a {@code String} containing the name of the directory + * to create. + * @return this FtpClient + * @throws IOException if an error occured during the exchange + * @throws FtpProtocolException if the command was rejected by the server + */ + public abstract FtpClient makeDirectory(String name) throws FtpProtocolException, IOException; /** - * LIST files in the current directory on a remote FTP server + * Removes a directory on the server. * - * @return the InputStream to read the list from + * @param name a {@code String} containing the name of the directory + * to remove. * + * @return this FtpClient + * @throws IOException if an error occured during the exchange. + * @throws FtpProtocolException if the command was rejected by the server */ - public TelnetInputStream list() throws IOException { - Socket s = openDataConnection("LIST"); - - return new TelnetInputStream(s.getInputStream(), binaryMode); - } + public abstract FtpClient removeDirectory(String name) throws FtpProtocolException, IOException; /** - * List (NLST) file names on a remote FTP server + * Sends a No-operation command. It's useful for testing the connection + * status or as a keep alive mechanism. * - * @param path pathname to the directory to list, null for current - * directory - * @return the InputStream to read the list from - * @exception FtpProtocolException + * @return this FtpClient + * @throws IOException if an error occured during the transmission. + * @throws FtpProtocolException if the command was rejected by the server */ - public TelnetInputStream nameList(String path) throws IOException { - Socket s; + public abstract FtpClient noop() throws FtpProtocolException, IOException; - if (path != null) - s = openDataConnection("NLST " + path); - else - s = openDataConnection("NLST"); - return new TelnetInputStream(s.getInputStream(), binaryMode); - } + /** + * Sends the {@code STAT} command to the server. + * This can be used while a data connection is open to get a status + * on the current transfer, in that case the parameter should be + * {@code null}. + * If used between file transfers, it may have a pathname as argument + * in which case it will work as the LIST command except no data + * connection will be created. + * + * @param name an optional {@code String} containing the pathname + * the STAT command should apply to. + * @return the response from the server + * @throws IOException if an error occured during the transmission. + * @throws FtpProtocolException if the command was rejected by the server + */ + public abstract String getStatus(String name) throws FtpProtocolException, IOException; /** - * CD to a specific directory on a remote FTP server + * Sends the {@code FEAT} command to the server and returns the list of supported + * features in the form of strings. * - * @param remoteDirectory path of the directory to CD to + * The features are the supported commands, like AUTH TLS, PROT or PASV. + * See the RFCs for a complete list. * - * @exception FtpProtocolException + * Note that not all FTP servers support that command, in which case + * a {@link FtpProtocolException} will be thrown. + * + * @return a {@code List} of {@code Strings} describing the + * supported additional features + * @throws IOException if an error occurs during the transmission. + * @throws FtpProtocolException if the command is rejected by the server */ - public void cd(String remoteDirectory) throws IOException { - if (remoteDirectory == null || - "".equals(remoteDirectory)) - return; - issueCommandCheck("CWD " + remoteDirectory); - } + public abstract List getFeatures() throws FtpProtocolException, IOException; + + /** + * Sends the {@code ABOR} command to the server. + *

It tells the server to stop the previous command or transfer. No action + * will be taken if the previous command has already been completed.

+ *

This doesn't abort the current session, more commands can be issued + * after an abort.

+ * + * @return this FtpClient + * @throws IOException if an error occured during the transmission. + * @throws FtpProtocolException if the command was rejected by the server + */ + public abstract FtpClient abort() throws FtpProtocolException, IOException; /** - * CD to the parent directory on a remote FTP server + * Some methods do not wait until completion before returning, so this + * method can be called to wait until completion. This is typically the case + * with commands that trigger a transfer like {@link #getFileStream(String)}. + * So this method should be called before accessing information related to + * such a command. + *

This method will actually block reading on the command channel for a + * notification from the server that the command is finished. Such a + * notification often carries extra information concerning the completion + * of the pending action (e.g. number of bytes transfered).

+ *

Note that this will return immediately if no command or action + * is pending

+ *

It should be also noted that most methods issuing commands to the ftp + * server will call this method if a previous command is pending. + *

Example of use: + *

+     * InputStream in = cl.getFileStream("file");
+     * ...
+     * cl.completePending();
+     * long size = cl.getLastTransferSize();
+     * 
+ * On the other hand, it's not necessary in a case like: + *
+     * InputStream in = cl.getFileStream("file");
+     * // read content
+     * ...
+     * cl.close();
+     * 
+ *

Since {@link #close()} will call completePending() if necessary.

+ * @return this FtpClient + * @throws IOException if an error occured during the transfer + * @throws FtpProtocolException if the command didn't complete successfully + */ + public abstract FtpClient completePending() throws FtpProtocolException, IOException; + + /** + * Reinitializes the USER parameters on the FTP server * + * @return this FtpClient + * @throws IOException if an error occurs during transmission + * @throws FtpProtocolException if the command fails */ - public void cdUp() throws IOException { - issueCommandCheck("CDUP"); - } + public abstract FtpClient reInit() throws FtpProtocolException, IOException; /** - * Print working directory of remote FTP server + * Changes the transfer type (binary, ascii, ebcdic) and issue the + * proper command (e.g. TYPE A) to the server. * - * @exception FtpProtocolException if the command fails + * @param type the {@code TransferType} to use. + * @return This FtpClient + * @throws IOException if an error occurs during transmission. + * @throws FtpProtocolException if the command was rejected by the server */ - public String pwd() throws IOException { - String answ; + public abstract FtpClient setType(TransferType type) throws FtpProtocolException, IOException; - 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('"')); + /** + * Changes the current transfer type to binary. + * This is a convenience method that is equivalent to + * {@code setType(TransferType.BINARY)} + * + * @return This FtpClient + * @throws IOException if an error occurs during the transmission. + * @throws FtpProtocolException if the command was rejected by the server + * @see #setType(TransferType) + */ + public FtpClient setBinaryType() throws FtpProtocolException, IOException { + setType(TransferType.BINARY); + return this; } /** - * Set transfer type to 'I' - * - * @exception FtpProtocolException if the command fails + * Changes the current transfer type to ascii. + * This is a convenience method that is equivalent to + * {@code setType(TransferType.ASCII)} + * + * @return This FtpClient + * @throws IOException if an error occurs during the transmission. + * @throws FtpProtocolException if the command was rejected by the server + * @see #setType(TransferType) */ - public void binary() throws IOException { - issueCommandCheck("TYPE I"); - binaryMode = true; + public FtpClient setAsciiType() throws FtpProtocolException, IOException { + setType(TransferType.ASCII); + return this; } /** - * Set transfer type to 'A' - * - * @exception FtpProtocolException if the command fails + * Issues a {@code LIST} command to the server to get the current directory + * listing, and returns the InputStream from the data connection. + * + *

{@link #completePending()} has to be called once the application + * is finished reading from the stream.

+ * + * @param path the pathname of the directory to list, or {@code null} + * for the current working directory. + * @return the {@code InputStream} from the resulting data connection + * @throws IOException if an error occurs during the transmission. + * @throws FtpProtocolException if the command was rejected by the server + * @see #changeDirectory(String) + * @see #listFiles(String) */ - public void ascii() throws IOException { - issueCommandCheck("TYPE A"); - binaryMode = false; - } + public abstract InputStream list(String path) throws FtpProtocolException, IOException; /** - * Rename a file on the ftp server - * - * @exception FtpProtocolException if the command fails + * Issues a {@code NLST path} command to server to get the specified directory + * content. It differs from {@link #list(String)} method by the fact that + * it will only list the file names which would make the parsing of the + * somewhat easier. + * + *

{@link #completePending()} has to be called once the application + * is finished reading from the stream.

+ * + * @param path a {@code String} containing the pathname of the + * directory to list or {@code null} for the current working directory. + * @return the {@code InputStream} from the resulting data connection + * @throws IOException if an error occurs during the transmission. + * @throws FtpProtocolException if the command was rejected by the server */ - public void rename(String from, String to) throws IOException { - issueCommandCheck("RNFR " + from); - issueCommandCheck("RNTO " + to); - } + public abstract InputStream nameList(String path) throws FtpProtocolException, IOException; /** - * Get the "System string" from the FTP server - * - * @exception FtpProtocolException if it fails + * Issues the {@code SIZE [path]} command to the server to get the size of a + * specific file on the server. + * Note that this command may not be supported by the server. In which + * case -1 will be returned. + * + * @param path a {@code String} containing the pathname of the + * file. + * @return a {@code long} containing the size of the file or -1 if + * the server returned an error, which can be checked with + * {@link #getLastReplyCode()}. + * @throws IOException if an error occurs during the transmission. + * @throws FtpProtocolException if the command was rejected by the server */ - 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 " - } + public abstract long getSize(String path) throws FtpProtocolException, IOException; + + /** + * Issues the {@code MDTM [path]} command to the server to get the modification + * time of a specific file on the server. + * Note that this command may not be supported by the server, in which + * case {@code null} will be returned. + * + * @param path a {@code String} containing the pathname of the file. + * @return a {@code Date} representing the last modification time + * or {@code null} if the server returned an error, which + * can be checked with {@link #getLastReplyCode()}. + * @throws IOException if an error occurs during the transmission. + * @throws FtpProtocolException if the command was rejected by the server + */ + public abstract Date getLastModified(String path) throws FtpProtocolException, IOException; /** - * Send a No-operation command. It's usefull for testing the connection status + * Sets the parser used to handle the directory output to the specified + * one. By default the parser is set to one that can handle most FTP + * servers output (Unix base mostly). However it may be necessary for + * and application to provide its own parser due to some uncommon + * output format. + * + * @param p The {@code FtpDirParser} to use. + * @return this FtpClient + * @see #listFiles(String) + */ + public abstract FtpClient setDirParser(FtpDirParser p); + + /** + * Issues a {@code MLSD} command to the server to get the specified directory + * listing and applies the internal parser to create an Iterator of + * {@link java.net.FtpDirEntry}. Note that the Iterator returned is also a + * {@link java.io.Closeable}. + *

If the server doesn't support the MLSD command, the LIST command is used + * instead and the parser set by {@link #setDirParser(java.net.FtpDirParser) } + * is used instead.

+ * + * {@link #completePending()} has to be called once the application + * is finished iterating through the files. + * + * @param path the pathname of the directory to list or {@code null} + * for the current working directoty. + * @return a {@code Iterator} of files or {@code null} if the + * command failed. + * @throws IOException if an error occured during the transmission + * @see #setDirParser(FtpDirParser) + * @see #changeDirectory(String) + * @throws FtpProtocolException if the command was rejected by the server + */ + public abstract Iterator listFiles(String path) throws FtpProtocolException, IOException; + + /** + * Attempts to use Kerberos GSSAPI as an authentication mechanism with the + * ftp server. This will issue an {@code AUTH GSSAPI} command, and if + * it is accepted by the server, will followup with {@code ADAT} + * command to exchange the various tokens until authentication is + * successful. This conforms to Appendix I of RFC 2228. + * + * @return this FtpClient + * @throws IOException if an error occurs during the transmission. + * @throws FtpProtocolException if the command was rejected by the server + */ + public abstract FtpClient useKerberos() throws FtpProtocolException, IOException; + + /** + * Returns the Welcome string the server sent during initial connection. * - * @exception FtpProtocolException if the command fails + * @return a {@code String} containing the message the server + * returned during connection or {@code null}. */ - public void noop() throws IOException { - issueCommandCheck("NOOP"); - } + public abstract String getWelcomeMsg(); /** - * Reinitialize the USER parameters on the FTp server + * Returns the last reply code sent by the server. * - * @exception FtpProtocolException if the command fails + * @return the lastReplyCode or {@code null} if none were received yet. */ - public void reInit() throws IOException { - issueCommandCheck("REIN"); - loggedIn = false; - } + public abstract FtpReplyCode getLastReplyCode(); /** - * New FTP client connected to host host. + * Returns the last response string sent by the server. * - * @param host Hostname of the FTP server + * @return the message string, which can be quite long, last returned + * by the server, or {@code null} if no response were received yet. + */ + public abstract String getLastResponseString(); + + /** + * Returns, when available, the size of the latest started transfer. + * This is retreived by parsing the response string received as an initial + * response to a {@code RETR} or similar request. * - * @exception FtpProtocolException if the connection fails + * @return the size of the latest transfer or -1 if either there was no + * transfer or the information was unavailable. */ - public FtpClient(String host) throws IOException { - super(); - openServer(host, FTP_PORT); - } + public abstract long getLastTransferSize(); /** - * New FTP client connected to host host, port port. + * Returns, when available, the remote name of the last transfered file. + * This is mainly useful for "put" operation when the unique flag was + * set since it allows to recover the unique file name created on the + * server which may be different from the one submitted with the command. * - * @param host Hostname of the FTP server - * @param port port number to connect to (usually 21) + * @return the name the latest transfered file remote name, or + * {@code null} if that information is unavailable. + */ + public abstract String getLastFileName(); + + /** + * Attempts to switch to a secure, encrypted connection. This is done by + * sending the {@code AUTH TLS} command. + *

See RFC 4217

+ * If successful this will establish a secure command channel with the + * server, it will also make it so that all other transfers (e.g. a RETR + * command) will be done over an encrypted channel as well unless a + * {@link #reInit()} command or a {@link #endSecureSession()} command is issued. + *

This method should be called after a successful {@link #connect(java.net.InetSocketAddress) } + * but before calling {@link #login(java.lang.String, char[]) }.

+ * + * @return this FtpCLient + * @throws IOException if an error occured during the transmission. + * @throws FtpProtocolException if the command was rejected by the server + * @see #endSecureSession() + */ + public abstract FtpClient startSecureSession() throws FtpProtocolException, IOException; + + /** + * Sends a {@code CCC} command followed by a {@code PROT C} + * command to the server terminating an encrypted session and reverting + * back to a non encrypted transmission. + * + * @return this FtpClient + * @throws IOException if an error occured during transmission. + * @throws FtpProtocolException if the command was rejected by the server + * @see #startSecureSession() + */ + public abstract FtpClient endSecureSession() throws FtpProtocolException, IOException; + + /** + * Sends the "Allocate" ({@code ALLO}) command to the server telling it to + * pre-allocate the specified number of bytes for the next transfer. * - * @exception FtpProtocolException if the connection fails + * @param size The number of bytes to allocate. + * @return this FtpClient + * @throws IOException if an error occured during the transmission. + * @throws FtpProtocolException if the command was rejected by the server */ - public FtpClient(String host, int port) throws IOException { - super(); - openServer(host, port); - } + public abstract FtpClient allocate(long size) throws FtpProtocolException, IOException; - /** Create an uninitialized FTP client. */ - public FtpClient() {} + /** + * Sends the "Structure Mount" ({@code SMNT}) command to the server. This let the + * user mount a different file system data structure without altering his + * login or accounting information. + * + * @param struct a {@code String} containing the name of the + * structure to mount. + * @return this FtpClient + * @throws IOException if an error occured during the transmission. + * @throws FtpProtocolException if the command was rejected by the server + */ + public abstract FtpClient structureMount(String struct) throws FtpProtocolException, IOException; - public FtpClient(Proxy p) { - proxy = p; - } + /** + * Sends a System ({@code SYST}) command to the server and returns the String + * sent back by the server describing the operating system at the + * server. + * + * @return a {@code String} describing the OS, or {@code null} + * if the operation was not successful. + * @throws IOException if an error occured during the transmission. + * @throws FtpProtocolException if the command was rejected by the server + */ + public abstract String getSystem() throws FtpProtocolException, IOException; - 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(); - } + /** + * Sends the {@code HELP} command to the server, with an optional command, like + * SITE, and returns the text sent back by the server. + * + * @param cmd the command for which the help is requested or + * {@code null} for the general help + * @return a {@code String} containing the text sent back by the + * server, or {@code null} if the command failed. + * @throws IOException if an error occured during transmission + * @throws FtpProtocolException if the command was rejected by the server + */ + public abstract String getHelp(String cmd) throws FtpProtocolException, IOException; + /** + * Sends the {@code SITE} command to the server. This is used by the server + * to provide services specific to his system that are essential + * to file transfer. + * + * @param cmd the command to be sent. + * @return this FtpClient + * @throws IOException if an error occured during transmission + * @throws FtpProtocolException if the command was rejected by the server + */ + public abstract FtpClient siteCmd(String cmd) throws FtpProtocolException, IOException; } diff --git a/jdk/src/share/classes/sun/net/ftp/FtpClientProvider.java b/jdk/src/share/classes/sun/net/ftp/FtpClientProvider.java new file mode 100644 index 0000000000..0061fcd219 --- /dev/null +++ b/jdk/src/share/classes/sun/net/ftp/FtpClientProvider.java @@ -0,0 +1,158 @@ +/* + * Copyright 2009 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.security.AccessController; +import java.security.PrivilegedAction; +import java.util.ServiceConfigurationError; +//import sun.misc.Service; + +/** + * Service provider class for FtpClient. + * Sub-classes of FtpClientProvider provide an implementation of {@link FtpClient} + * and associated classes. Applications do not normally use this class directly. + * See {@link #provider() } for how providers are found and loaded. + * + * @since 1.7 + */ +public abstract class FtpClientProvider { + + /** + * Creates a FtpClient from this provider. + * + * @return The created {@link FtpClient}. + */ + public abstract FtpClient createFtpClient(); + private static final Object lock = new Object(); + private static FtpClientProvider provider = null; + + /** + * Initializes a new instance of this class. + * + * @throws SecurityException if a security manager is installed and it denies + * {@link RuntimePermission}("ftpClientProvider") + */ + protected FtpClientProvider() { + SecurityManager sm = System.getSecurityManager(); + if (sm != null) { + sm.checkPermission(new RuntimePermission("ftpClientProvider")); + } + } + + private static boolean loadProviderFromProperty() { + String cm = System.getProperty("sun.net.ftpClientProvider"); + if (cm == null) { + return false; + } + try { + Class c = Class.forName(cm, true, null); + provider = (FtpClientProvider) c.newInstance(); + return true; + } catch (ClassNotFoundException x) { + throw new ServiceConfigurationError(x.toString()); + } catch (IllegalAccessException x) { + throw new ServiceConfigurationError(x.toString()); + } catch (InstantiationException x) { + throw new ServiceConfigurationError(x.toString()); + } catch (SecurityException x) { + throw new ServiceConfigurationError(x.toString()); + } + } + + private static boolean loadProviderAsService() { + // Iterator i = Service.providers(FtpClientProvider.class, + // ClassLoader.getSystemClassLoader()); + // while (i.hasNext()) { + // try { + // provider = (FtpClientProvider) i.next(); + // return true; + // } catch (ServiceConfigurationError sce) { + // if (sce.getCause() instanceof SecurityException) { + // // Ignore, try next provider, if any + // continue; + // } + // throw sce; + // } + // } + return false; + } + + /** + * Returns the system wide default FtpClientProvider for this invocation of + * the Java virtual machine. + * + *

The first invocation of this method locates the default provider + * object as follows:

+ * + *
    + * + *
  1. If the system property + * java.net.FtpClientProvider is defined then it is + * taken to be the fully-qualified name of a concrete provider class. + * The class is loaded and instantiated; if this process fails then an + * unspecified unchecked error or exception is thrown.

  2. + * + *
  3. If a provider class has been installed in a jar file that is + * visible to the system class loader, and that jar file contains a + * provider-configuration file named + * java.net.FtpClientProvider in the resource + * directory META-INF/services, then the first class name + * specified in that file is taken. The class is loaded and + * instantiated; if this process fails then an unspecified unchecked error or exception is + * thrown.

  4. + * + *
  5. Finally, if no provider has been specified by any of the above + * means then the system-default provider class is instantiated and the + * result is returned.

  6. + * + *
+ * + *

Subsequent invocations of this method return the provider that was + * returned by the first invocation.

+ * + * @return The system-wide default FtpClientProvider + */ + public static FtpClientProvider provider() { + synchronized (lock) { + if (provider != null) { + return provider; + } + return (FtpClientProvider) AccessController.doPrivileged( + new PrivilegedAction() { + + public Object run() { + if (loadProviderFromProperty()) { + return provider; + } + if (loadProviderAsService()) { + return provider; + } + provider = new sun.net.ftp.impl.DefaultFtpClientProvider(); + return provider; + } + }); + } + } +} diff --git a/jdk/src/share/classes/sun/net/ftp/FtpDirEntry.java b/jdk/src/share/classes/sun/net/ftp/FtpDirEntry.java new file mode 100644 index 0000000000..f497112e58 --- /dev/null +++ b/jdk/src/share/classes/sun/net/ftp/FtpDirEntry.java @@ -0,0 +1,331 @@ +/* + * Copyright 2009 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.Date; +import java.util.HashMap; + +/** + * A {@code FtpDirEntry} is a class agregating all the information that the FTP client + * can gather from the server by doing a {@code LST} (or {@code NLST}) command and + * parsing the output. It will typically contain the name, type, size, last modification + * time, owner and group of the file, although some of these could be unavailable + * due to specific FTP server limitations. + * + * @see sun.net.ftp.FtpDirParser + * @since 1.7 + */ +public class FtpDirEntry { + + public enum Type { + + FILE, DIR, PDIR, CDIR, LINK + }; + + public enum Permission { + + USER(0), GROUP(1), OTHERS(2); + int value; + + Permission(int v) { + value = v; + } + }; + private final String name; + private String user = null; + private String group = null; + private long size = -1; + private java.util.Date created = null; + private java.util.Date lastModified = null; + private Type type = Type.FILE; + private boolean[][] permissions = null; + private HashMap facts = new HashMap(); + + private FtpDirEntry() { + name = null; + } + + /** + * Creates an FtpDirEntry instance with only the name being set. + * + * @param name The name of the file + */ + public FtpDirEntry(String name) { + this.name = name; + } + + /** + * Returns the name of the remote file. + * + * @return a {@code String} containing the name of the remote file. + */ + public String getName() { + return name; + } + + /** + * Returns the user name of the owner of the file as returned by the FTP + * server, if provided. This could be a name or a user id (number). + * + * @return a {@code String} containing the user name or + * {@code null} if that information is not available. + */ + public String getUser() { + return user; + } + + /** + * Sets the user name of the owner of the file. Intended mostly to be + * used from inside a {@link java.net.FtpDirParser} implementation. + * + * @param user The user name of the owner of the file, or {@code null} + * if that information is not available. + * @return this FtpDirEntry + */ + public FtpDirEntry setUser(String user) { + this.user = user; + return this; + } + + /** + * Returns the group name of the file as returned by the FTP + * server, if provided. This could be a name or a group id (number). + * + * @return a {@code String} containing the group name or + * {@code null} if that information is not available. + */ + public String getGroup() { + return group; + } + + /** + * Sets the name of the group to which the file belong. Intended mostly to be + * used from inside a {@link java.net.FtpDirParser} implementation. + * + * @param group The name of the group to which the file belong, or {@code null} + * if that information is not available. + * @return this FtpDirEntry + */ + public FtpDirEntry setGroup(String group) { + this.group = group; + return this; + } + + /** + * Returns the size of the remote file as it was returned by the FTP + * server, if provided. + * + * @return the size of the file or -1 if that information is not available. + */ + public long getSize() { + return size; + } + + /** + * Sets the size of that file. Intended mostly to be used from inside an + * {@link java.net.FtpDirParser} implementation. + * + * @param size The size, in bytes, of that file. or -1 if unknown. + * @return this FtpDirEntry + */ + public FtpDirEntry setSize(long size) { + this.size = size; + return this; + } + + /** + * Returns the type of the remote file as it was returned by the FTP + * server, if provided. + * It returns a FtpDirEntry.Type enum and the values can be: + * - FtpDirEntry.Type.FILE for a normal file + * - FtpDirEntry.Type.DIR for a directory + * - FtpDirEntry.Type.LINK for a symbolic link + * + * @return a {@code FtpDirEntry.Type} describing the type of the file + * or {@code null} if that information is not available. + */ + public Type getType() { + return type; + } + + /** + * Sets the type of the file. Intended mostly to be used from inside an + * {@link java.net.FtpDirParser} implementation. + * + * @param type the type of this file or {@code null} if that information + * is not available. + * @return this FtpDirEntry + */ + public FtpDirEntry setType(Type type) { + this.type = type; + return this; + } + + /** + * Returns the last modification time of the remote file as it was returned + * by the FTP server, if provided, {@code null} otherwise. + * + * @return a Date representing the last time the file was + * modified on the server, or {@code null} if that + * information is not available. + */ + public java.util.Date getLastModified() { + return this.lastModified; + } + + /** + * Sets the last modification time of the file. Intended mostly to be used + * from inside an {@link java.net.FtpDirParser} implementation. + * + * @param lastModified The Date representing the last modification time, or + * {@code null} if that information is not available. + * @return this FtpDirEntry + */ + public FtpDirEntry setLastModified(Date lastModified) { + this.lastModified = lastModified; + return this; + } + + /** + * Returns whether read access is granted for a specific permission. + * + * @param p the Permission (user, group, others) to check. + * @return {@code true} if read access is granted. + */ + public boolean canRead(Permission p) { + if (permissions != null) { + return permissions[p.value][0]; + } + return false; + } + + /** + * Returns whether write access is granted for a specific permission. + * + * @param p the Permission (user, group, others) to check. + * @return {@code true} if write access is granted. + */ + public boolean canWrite(Permission p) { + if (permissions != null) { + return permissions[p.value][1]; + } + return false; + } + + /** + * Returns whether execute access is granted for a specific permission. + * + * @param p the Permission (user, group, others) to check. + * @return {@code true} if execute access is granted. + */ + public boolean canExexcute(Permission p) { + if (permissions != null) { + return permissions[p.value][2]; + } + return false; + } + + /** + * Sets the permissions for that file. Intended mostly to be used + * from inside an {@link java.net.FtpDirParser} implementation. + * The permissions array is a 3x3 {@code boolean} array, the first index being + * the User, group or owner (0, 1 and 2 respectively) while the second + * index is read, write or execute (0, 1 and 2 respectively again). + *

E.G.: {@code permissions[1][2]} is the group/execute permission.

+ * + * @param permissions a 3x3 {@code boolean} array + * @return this {@code FtpDirEntry} + */ + public FtpDirEntry setPermissions(boolean[][] permissions) { + this.permissions = permissions; + return this; + } + + /** + * Adds a 'fact', as defined in RFC 3659, to the list of facts of this file. + * Intended mostly to be used from inside a {@link java.net.FtpDirParser} + * implementation. + * + * @param fact the name of the fact (e.g. "Media-Type"). It is not case-sensitive. + * @param value the value associated with this fact. + * @return this {@code FtpDirEntry} + */ + public FtpDirEntry addFact(String fact, String value) { + facts.put(fact.toLowerCase(), value); + return this; + } + + /** + * Returns the requested 'fact', as defined in RFC 3659, if available. + * + * @param fact The name of the fact *e.g. "Media-Type"). It is not case sensitive. + * @return The value of the fact or, {@code null} if that fact wasn't + * provided by the server. + */ + public String getFact(String fact) { + return facts.get(fact.toLowerCase()); + } + + /** + * Returns the creation time of the file, when provided by the server. + * + * @return The Date representing the creation time, or {@code null} + * if the server didn't provide that information. + */ + public Date getCreated() { + return created; + } + + /** + * Sets the creation time for that file. Intended mostly to be used from + * inside a {@link java.net.FtpDirParser} implementation. + * + * @param created the Date representing the creation time for that file, or + * {@code null} if that information is not available. + * @return this FtpDirEntry + */ + public FtpDirEntry setCreated(Date created) { + this.created = created; + return this; + } + + /** + * Returns a string representation of the object. + * The {@code toString} method for class {@code FtpDirEntry} + * returns a string consisting of the name of the file, followed by its + * type between brackets, followed by the user and group between + * parenthesis, then size between '{', and, finally, the lastModified of last + * modification if it's available. + * + * @return a string representation of the object. + */ + @Override + public String toString() { + if (lastModified == null) { + return name + " [" + type + "] (" + user + " / " + group + ") " + size; + } + return name + " [" + type + "] (" + user + " / " + group + ") {" + size + "} " + java.text.DateFormat.getDateInstance().format(lastModified); + } +} diff --git a/jdk/src/share/classes/sun/net/ftp/FtpDirParser.java b/jdk/src/share/classes/sun/net/ftp/FtpDirParser.java new file mode 100644 index 0000000000..ffd6a9ef74 --- /dev/null +++ b/jdk/src/share/classes/sun/net/ftp/FtpDirParser.java @@ -0,0 +1,49 @@ +/* + * Copyright 2009 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; + +/** + * This interface describes a parser for the FtpClient class. Such a parser is + * used when listing a remote directory to transform text lines like: + * drwxr-xr-x 1 user01 ftp 512 Jan 29 23:32 prog + * into FtpDirEntry instances. + * + * @see java.net.FtpClient#setFileParser(FtpDirParser) + * @since 1.7 + */ +public interface FtpDirParser { + + /** + * Takes one line from a directory listing and returns an FtpDirEntry instance + * based on the information contained. + * + * @param line a String, a line sent by the FTP server as a + * result of the LST command. + * @return an FtpDirEntry instance. + * @see java.net.FtpDirEntry + */ + public FtpDirEntry parseLine(String line); +} diff --git a/jdk/src/share/classes/sun/net/ftp/FtpLoginException.java b/jdk/src/share/classes/sun/net/ftp/FtpLoginException.java index 0d37a8706c..4096a8da28 100644 --- a/jdk/src/share/classes/sun/net/ftp/FtpLoginException.java +++ b/jdk/src/share/classes/sun/net/ftp/FtpLoginException.java @@ -1,5 +1,5 @@ /* - * Copyright 1994-2008 Sun Microsystems, Inc. All Rights Reserved. + * Copyright 1994-2009 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 @@ -25,7 +25,7 @@ package sun.net.ftp; -import java.io.*; +import java.io.IOException; /** * This exception is thrown when an error is encountered during an @@ -33,10 +33,10 @@ import java.io.*; * * @author Jonathan Payne */ -public class FtpLoginException extends FtpProtocolException { +public class FtpLoginException extends IOException { private static final long serialVersionUID = 2218162403237941536L; - FtpLoginException(String s) { + public FtpLoginException(String s) { super(s); } } diff --git a/jdk/src/share/classes/sun/net/ftp/FtpProtocolException.java b/jdk/src/share/classes/sun/net/ftp/FtpProtocolException.java index 6afbe215e0..dc63fbd1a1 100644 --- a/jdk/src/share/classes/sun/net/ftp/FtpProtocolException.java +++ b/jdk/src/share/classes/sun/net/ftp/FtpProtocolException.java @@ -1,5 +1,5 @@ /* - * Copyright 1994-2008 Sun Microsystems, Inc. All Rights Reserved. + * Copyright 1994-2009 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 @@ -22,21 +22,49 @@ * CA 95054 USA or visit www.sun.com if you need additional information or * have any questions. */ - package sun.net.ftp; -import java.io.*; - /** - * This exeception is thrown when unexpected results are returned during - * an FTP session. - * + * Thrown to indicate that the FTP server reported an error. + * For instance that the requested file doesn't exist or + * that a command isn't supported. + *

The specific error code can be retreived with {@link #getReplyCode() }.

* @author Jonathan Payne */ -public class FtpProtocolException extends IOException { +public class FtpProtocolException extends Exception { private static final long serialVersionUID = 5978077070276545054L; + private final FtpReplyCode code; + + /** + * Constructs a new {@code FtpProtocolException} from the + * specified detail message. The reply code is set to unknow error. + * + * @param detail the detail message. + */ + public FtpProtocolException(String detail) { + super(detail); + code = FtpReplyCode.UNKNOWN_ERROR; + } + + /** + * Constructs a new {@code FtpProtocolException} from the + * specified response code and exception detail message + * + * @param detail the detail message. + * @param code The {@code FtpRelyCode} received from server. + */ + public FtpProtocolException(String detail, FtpReplyCode code) { + super(detail); + this.code = code; + } - FtpProtocolException(String s) { - super(s); + /** + * Gets the reply code sent by the server that led to this exception + * being thrown. + * + * @return The {@link FtpReplyCode} associated with that exception. + */ + public FtpReplyCode getReplyCode() { + return code; } } diff --git a/jdk/src/share/classes/sun/net/ftp/FtpReplyCode.java b/jdk/src/share/classes/sun/net/ftp/FtpReplyCode.java new file mode 100644 index 0000000000..d3df547df0 --- /dev/null +++ b/jdk/src/share/classes/sun/net/ftp/FtpReplyCode.java @@ -0,0 +1,248 @@ +/* + * Copyright 2009 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; + +/** + * This class describes a FTP protocol reply code and associates a meaning + * to the numerical value according to the various RFCs (RFC 959 in + * particular). + * + */ +public enum FtpReplyCode { + + RESTART_MARKER(110), + SERVICE_READY_IN(120), + DATA_CONNECTION_ALREADY_OPEN(125), + FILE_STATUS_OK(150), + COMMAND_OK(200), + NOT_IMPLEMENTED(202), + SYSTEM_STATUS(211), + DIRECTORY_STATUS(212), + FILE_STATUS(213), + HELP_MESSAGE(214), + NAME_SYSTEM_TYPE(215), + SERVICE_READY(220), + SERVICE_CLOSING(221), + DATA_CONNECTION_OPEN(225), + CLOSING_DATA_CONNECTION(226), + ENTERING_PASSIVE_MODE(227), + ENTERING_EXT_PASSIVE_MODE(229), + LOGGED_IN(230), + SECURELY_LOGGED_IN(232), + SECURITY_EXCHANGE_OK(234), + SECURITY_EXCHANGE_COMPLETE(235), + FILE_ACTION_OK(250), + PATHNAME_CREATED(257), + NEED_PASSWORD(331), + NEED_ACCOUNT(332), + NEED_ADAT(334), + NEED_MORE_ADAT(335), + FILE_ACTION_PENDING(350), + SERVICE_NOT_AVAILABLE(421), + CANT_OPEN_DATA_CONNECTION(425), + CONNECTION_CLOSED(426), + NEED_SECURITY_RESOURCE(431), + FILE_ACTION_NOT_TAKEN(450), + ACTION_ABORTED(451), + INSUFFICIENT_STORAGE(452), + COMMAND_UNRECOGNIZED(500), + INVALID_PARAMETER(501), + BAD_SEQUENCE(503), + NOT_IMPLEMENTED_FOR_PARAMETER(504), + NOT_LOGGED_IN(530), + NEED_ACCOUNT_FOR_STORING(532), + PROT_LEVEL_DENIED(533), + REQUEST_DENIED(534), + FAILED_SECURITY_CHECK(535), + UNSUPPORTED_PROT_LEVEL(536), + PROT_LEVEL_NOT_SUPPORTED_BY_SECURITY(537), + FILE_UNAVAILABLE(550), + PAGE_TYPE_UNKNOWN(551), + EXCEEDED_STORAGE(552), + FILE_NAME_NOT_ALLOWED(553), + PROTECTED_REPLY(631), + UNKNOWN_ERROR(999); + private final int value; + + FtpReplyCode(int val) { + this.value = val; + } + + /** + * Returns the numerical value of the code. + * + * @return the numerical value. + */ + public int getValue() { + return value; + } + + /** + * Determines if the code is a Positive Preliminary response. + * This means beginning with a 1 (which means a value between 100 and 199) + * + * @return true if the reply code is a positive preliminary + * response. + */ + public boolean isPositivePreliminary() { + return value >= 100 && value < 200; + } + + /** + * Determines if the code is a Positive Completion response. + * This means beginning with a 2 (which means a value between 200 and 299) + * + * @return true if the reply code is a positive completion + * response. + */ + public boolean isPositiveCompletion() { + return value >= 200 && value < 300; + } + + /** + * Determines if the code is a positive internediate response. + * This means beginning with a 3 (which means a value between 300 and 399) + * + * @return true if the reply code is a positive intermediate + * response. + */ + public boolean isPositiveIntermediate() { + return value >= 300 && value < 400; + } + + /** + * Determines if the code is a transient negative response. + * This means beginning with a 4 (which means a value between 400 and 499) + * + * @return true if the reply code is a transient negative + * response. + */ + public boolean isTransientNegative() { + return value >= 400 && value < 500; + } + + /** + * Determines if the code is a permanent negative response. + * This means beginning with a 5 (which means a value between 500 and 599) + * + * @return true if the reply code is a permanent negative + * response. + */ + public boolean isPermanentNegative() { + return value >= 500 && value < 600; + } + + /** + * Determines if the code is a protected reply response. + * This means beginning with a 6 (which means a value between 600 and 699) + * + * @return true if the reply code is a protected reply + * response. + */ + public boolean isProtectedReply() { + return value >= 600 && value < 700; + } + + /** + * Determines if the code is a syntax related response. + * This means the second digit is a 0. + * + * @return true if the reply code is a syntax related + * response. + */ + public boolean isSyntax() { + return ((value / 10) - ((value / 100) * 10)) == 0; + } + + /** + * Determines if the code is an information related response. + * This means the second digit is a 1. + * + * @return true if the reply code is an information related + * response. + */ + public boolean isInformation() { + return ((value / 10) - ((value / 100) * 10)) == 1; + } + + /** + * Determines if the code is a connection related response. + * This means the second digit is a 2. + * + * @return true if the reply code is a connection related + * response. + */ + public boolean isConnection() { + return ((value / 10) - ((value / 100) * 10)) == 2; + } + + /** + * Determines if the code is an authentication related response. + * This means the second digit is a 3. + * + * @return true if the reply code is an authentication related + * response. + */ + public boolean isAuthentication() { + return ((value / 10) - ((value / 100) * 10)) == 3; + } + + /** + * Determines if the code is an unspecified type of response. + * This means the second digit is a 4. + * + * @return true if the reply code is an unspecified type of + * response. + */ + public boolean isUnspecified() { + return ((value / 10) - ((value / 100) * 10)) == 4; + } + + /** + * Determines if the code is a file system related response. + * This means the second digit is a 5. + * + * @return true if the reply code is a file system related + * response. + */ + public boolean isFileSystem() { + return ((value / 10) - ((value / 100) * 10)) == 5; + } + + /** + * Static utility method to convert a value into a FtpReplyCode. + * + * @param v the value to convert + * @return the FtpReplyCode associated with the value. + */ + public static FtpReplyCode find(int v) { + for (FtpReplyCode code : FtpReplyCode.values()) { + if (code.getValue() == v) { + return code; + } + } + return UNKNOWN_ERROR; + } +} diff --git a/jdk/src/share/classes/sun/net/ftp/impl/DefaultFtpClientProvider.java b/jdk/src/share/classes/sun/net/ftp/impl/DefaultFtpClientProvider.java new file mode 100644 index 0000000000..361ab9b589 --- /dev/null +++ b/jdk/src/share/classes/sun/net/ftp/impl/DefaultFtpClientProvider.java @@ -0,0 +1,38 @@ +/* + * Copyright 2009 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.impl; + +/** + * Default FtpClientProvider. + * Uses sun.net.ftp.FtpCLient. + */ +public class DefaultFtpClientProvider extends sun.net.ftp.FtpClientProvider { + + @Override + public sun.net.ftp.FtpClient createFtpClient() { + return sun.net.ftp.impl.FtpClient.create(); + } + +} diff --git a/jdk/src/share/classes/sun/net/ftp/impl/FtpClient.java b/jdk/src/share/classes/sun/net/ftp/impl/FtpClient.java new file mode 100644 index 0000000000..31fe2ba61a --- /dev/null +++ b/jdk/src/share/classes/sun/net/ftp/impl/FtpClient.java @@ -0,0 +1,2191 @@ +/* + * Copyright 2009 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.impl; + +import java.net.*; +import java.io.*; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.TimeZone; +import java.util.Vector; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import sun.misc.BASE64Decoder; +import sun.misc.BASE64Encoder; +import sun.net.ftp.*; +import sun.util.logging.PlatformLogger; + + +public class FtpClient extends sun.net.ftp.FtpClient { + + private static int defaultSoTimeout; + private static int defaultConnectTimeout; + private static final PlatformLogger logger = + PlatformLogger.getLogger("sun.net.ftp.FtpClient"); + private Proxy proxy; + private Socket server; + private PrintStream out; + private InputStream in; + private int readTimeout = -1; + private int connectTimeout = -1; + + /* Name of encoding to use for output */ + private static String encoding = "ISO8859_1"; + /** remember the ftp server name because we may need it */ + private InetSocketAddress serverAddr; + private boolean replyPending = false; + private boolean loggedIn = false; + private boolean useCrypto = false; + private SSLSocketFactory sslFact; + private Socket oldSocket; + /** Array of strings (usually 1 entry) for the last reply from the server. */ + private Vector serverResponse = new Vector(1); + /** The last reply code from the ftp daemon. */ + private FtpReplyCode lastReplyCode = null; + /** Welcome message from the server, if any. */ + private String welcomeMsg; + private boolean passiveMode = true; + private TransferType type = TransferType.BINARY; + private long restartOffset = 0; + private long lastTransSize = -1; // -1 means 'unknown size' + private String lastFileName; + /** + * Static members used by the parser + */ + private static String[] patStrings = { + // drwxr-xr-x 1 user01 ftp 512 Jan 29 23:32 prog + "([\\-ld](?:[r\\-][w\\-][x\\-]){3})\\s*\\d+ (\\w+)\\s*(\\w+)\\s*(\\d+)\\s*([A-Z][a-z][a-z]\\s*\\d+)\\s*(\\d\\d:\\d\\d)\\s*(\\p{Print}*)", + // drwxr-xr-x 1 user01 ftp 512 Jan 29 1997 prog + "([\\-ld](?:[r\\-][w\\-][x\\-]){3})\\s*\\d+ (\\w+)\\s*(\\w+)\\s*(\\d+)\\s*([A-Z][a-z][a-z]\\s*\\d+)\\s*(\\d{4})\\s*(\\p{Print}*)", + // 04/28/2006 09:12a 3,563 genBuffer.sh + "(\\d{2}/\\d{2}/\\d{4})\\s*(\\d{2}:\\d{2}[ap])\\s*((?:[0-9,]+)|(?:))\\s*(\\p{Graph}*)", + // 01-29-97 11:32PM prog + "(\\d{2}-\\d{2}-\\d{2})\\s*(\\d{2}:\\d{2}[AP]M)\\s*((?:[0-9,]+)|(?:))\\s*(\\p{Graph}*)" + }; + private static int[][] patternGroups = { + // 0 - file, 1 - size, 2 - date, 3 - time, 4 - year, 5 - permissions, + // 6 - user, 7 - group + {7, 4, 5, 6, 0, 1, 2, 3}, + {7, 4, 5, 0, 6, 1, 2, 3}, + {4, 3, 1, 2, 0, 0, 0, 0}, + {4, 3, 1, 2, 0, 0, 0, 0}}; + private static Pattern[] patterns; + private static Pattern linkp = Pattern.compile("(\\p{Print}+) \\-\\> (\\p{Print}+)$"); + private DateFormat df = DateFormat.getDateInstance(DateFormat.MEDIUM, java.util.Locale.US); + + static { + final int vals[] = {0, 0}; + final String encs[] = {null}; + + AccessController.doPrivileged( + new PrivilegedAction() { + + public Object run() { + vals[0] = Integer.getInteger("sun.net.client.defaultReadTimeout", 0).intValue(); + vals[1] = Integer.getInteger("sun.net.client.defaultConnectTimeout", 0).intValue(); + encs[0] = System.getProperty("file.encoding", "ISO8859_1"); + return null; + } + }); + if (vals[0] == 0) { + defaultSoTimeout = -1; + } else { + defaultSoTimeout = vals[0]; + } + + if (vals[1] == 0) { + defaultConnectTimeout = -1; + } else { + defaultConnectTimeout = vals[1]; + } + + encoding = encs[0]; + try { + if (!isASCIISuperset(encoding)) { + encoding = "ISO8859_1"; + } + } catch (Exception e) { + encoding = "ISO8859_1"; + } + + patterns = new Pattern[patStrings.length]; + for (int i = 0; i < patStrings.length; i++) { + patterns[i] = Pattern.compile(patStrings[i]); + } + } + + /** + * Test the named character encoding to verify that it converts ASCII + * characters correctly. We have to use an ASCII based encoding, or else + * the NetworkClients will not work correctly in EBCDIC based systems. + * However, we cannot just use ASCII or ISO8859_1 universally, because in + * Asian locales, non-ASCII characters may be embedded in otherwise + * ASCII based protocols (eg. HTTP). The specifications (RFC2616, 2398) + * are a little ambiguous in this matter. For instance, RFC2398 [part 2.1] + * says that the HTTP request URI should be escaped using a defined + * mechanism, but there is no way to specify in the escaped string what + * the original character set is. It is not correct to assume that + * UTF-8 is always used (as in URLs in HTML 4.0). For this reason, + * until the specifications are updated to deal with this issue more + * comprehensively, and more importantly, HTTP servers are known to + * support these mechanisms, we will maintain the current behavior + * where it is possible to send non-ASCII characters in their original + * unescaped form. + */ + private static boolean isASCIISuperset(String encoding) throws Exception { + String chkS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + "abcdefghijklmnopqrstuvwxyz-_.!~*'();/?:@&=+$,"; + + // Expected byte sequence for string above + byte[] chkB = {48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 65, 66, 67, 68, 69, 70, 71, 72, + 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, + 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, + 115, 116, 117, 118, 119, 120, 121, 122, 45, 95, 46, 33, 126, 42, 39, 40, 41, 59, + 47, 63, 58, 64, 38, 61, 43, 36, 44}; + + byte[] b = chkS.getBytes(encoding); + return java.util.Arrays.equals(b, chkB); + } + + private class DefaultParser implements FtpDirParser { + + /** + * Possible patterns: + * + * drwxr-xr-x 1 user01 ftp 512 Jan 29 23:32 prog + * drwxr-xr-x 1 user01 ftp 512 Jan 29 1997 prog + * drwxr-xr-x 1 1 1 512 Jan 29 23:32 prog + * lrwxr-xr-x 1 user01 ftp 512 Jan 29 23:32 prog -> prog2000 + * drwxr-xr-x 1 username ftp 512 Jan 29 23:32 prog + * -rw-r--r-- 1 jcc staff 105009 Feb 3 15:05 test.1 + * + * 01-29-97 11:32PM prog + * 04/28/2006 09:12a 3,563 genBuffer.sh + * + * drwxr-xr-x folder 0 Jan 29 23:32 prog + * + * 0 DIR 01-29-97 23:32 PROG + */ + private DefaultParser() { + } + + public FtpDirEntry parseLine(String line) { + String fdate = null; + String fsize = null; + String time = null; + String filename = null; + String permstring = null; + String username = null; + String groupname = null; + boolean dir = false; + Calendar now = Calendar.getInstance(); + int year = now.get(Calendar.YEAR); + + Matcher m = null; + for (int j = 0; j < patterns.length; j++) { + m = patterns[j].matcher(line); + if (m.find()) { + // 0 - file, 1 - size, 2 - date, 3 - time, 4 - year, + // 5 - permissions, 6 - user, 7 - group + filename = m.group(patternGroups[j][0]); + fsize = m.group(patternGroups[j][1]); + fdate = m.group(patternGroups[j][2]); + if (patternGroups[j][4] > 0) { + fdate += (", " + m.group(patternGroups[j][4])); + } else if (patternGroups[j][3] > 0) { + fdate += (", " + String.valueOf(year)); + } + if (patternGroups[j][3] > 0) { + time = m.group(patternGroups[j][3]); + } + if (patternGroups[j][5] > 0) { + permstring = m.group(patternGroups[j][5]); + dir = permstring.startsWith("d"); + } + if (patternGroups[j][6] > 0) { + username = m.group(patternGroups[j][6]); + } + if (patternGroups[j][7] > 0) { + groupname = m.group(patternGroups[j][7]); + } + // Old DOS format + if ("".equals(fsize)) { + dir = true; + fsize = null; + } + } + } + + if (filename != null) { + Date d; + try { + d = df.parse(fdate); + } catch (Exception e) { + d = null; + } + if (d != null && time != null) { + int c = time.indexOf(":"); + now.setTime(d); + now.set(Calendar.HOUR, Integer.parseInt(time.substring(0, c))); + now.set(Calendar.MINUTE, Integer.parseInt(time.substring(c + 1))); + d = now.getTime(); + } + // see if it's a symbolic link, i.e. the name if followed + // by a -> and a path + Matcher m2 = linkp.matcher(filename); + if (m2.find()) { + // Keep only the name then + filename = m2.group(1); + } + boolean[][] perms = new boolean[3][3]; + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + perms[i][j] = (permstring.charAt((i * 3) + j) != '-'); + } + } + FtpDirEntry file = new FtpDirEntry(filename); + file.setUser(username).setGroup(groupname); + file.setSize(Long.parseLong(fsize)).setLastModified(d); + file.setPermissions(perms); + file.setType(dir ? FtpDirEntry.Type.DIR : (line.charAt(0) == 'l' ? FtpDirEntry.Type.LINK : FtpDirEntry.Type.FILE)); + return file; + } + return null; + } + } + + private class MLSxParser implements FtpDirParser { + + private SimpleDateFormat df = new SimpleDateFormat("yyyyMMddhhmmss"); + + public FtpDirEntry parseLine(String line) { + String name = null; + int i = line.lastIndexOf(";"); + if (i > 0) { + name = line.substring(i + 1).trim(); + line = line.substring(0, i); + } else { + name = line.trim(); + line = ""; + } + FtpDirEntry file = new FtpDirEntry(name); + while (!line.isEmpty()) { + String s; + i = line.indexOf(";"); + if (i > 0) { + s = line.substring(0, i); + line = line.substring(i + 1); + } else { + s = line; + line = ""; + } + i = s.indexOf("="); + if (i > 0) { + String fact = s.substring(0, i); + String value = s.substring(i + 1); + file.addFact(fact, value); + } + } + String s = file.getFact("Size"); + if (s != null) { + file.setSize(Long.parseLong(s)); + } + s = file.getFact("Modify"); + if (s != null) { + Date d = null; + try { + d = df.parse(s); + } catch (ParseException ex) { + } + if (d != null) { + file.setLastModified(d); + } + } + s = file.getFact("Create"); + if (s != null) { + Date d = null; + try { + d = df.parse(s); + } catch (ParseException ex) { + } + if (d != null) { + file.setCreated(d); + } + } + s = file.getFact("Type"); + if (s != null) { + if (s.equalsIgnoreCase("file")) { + file.setType(FtpDirEntry.Type.FILE); + } + if (s.equalsIgnoreCase("dir")) { + file.setType(FtpDirEntry.Type.DIR); + } + if (s.equalsIgnoreCase("cdir")) { + file.setType(FtpDirEntry.Type.CDIR); + } + if (s.equalsIgnoreCase("pdir")) { + file.setType(FtpDirEntry.Type.PDIR); + } + } + return file; + } + }; + private FtpDirParser parser = new DefaultParser(); + private FtpDirParser mlsxParser = new MLSxParser(); + private static Pattern transPat = null; + + private void getTransferSize() { + lastTransSize = -1; + /** + * If it's a start of data transfer response, let's try to extract + * the size from the response string. Usually it looks like that: + * + * 150 Opening BINARY mode data connection for foo (6701 bytes). + */ + String response = getLastResponseString(); + if (transPat == null) { + transPat = Pattern.compile("150 Opening .*\\((\\d+) bytes\\)."); + } + Matcher m = transPat.matcher(response); + if (m.find()) { + String s = m.group(1); + lastTransSize = Long.parseLong(s); + } + } + + /** + * extract the created file name from the response string: + * 226 Transfer complete (unique file name:toto.txt.1). + * Usually happens when a STOU (store unique) command had been issued. + */ + private void getTransferName() { + lastFileName = null; + String response = getLastResponseString(); + int i = response.indexOf("unique file name:"); + int e = response.lastIndexOf(')'); + if (i >= 0) { + i += 17; // Length of "unique file name:" + lastFileName = response.substring(i, e); + } + } + + /** + * Pulls the response from the server and returns the code as a + * number. Returns -1 on failure. + */ + private int readServerResponse() throws IOException { + StringBuffer replyBuf = new StringBuffer(32); + int c; + int continuingCode = -1; + int code; + String response; + + serverResponse.setSize(0); + while (true) { + while ((c = in.read()) != -1) { + if (c == '\r') { + if ((c = in.read()) != '\n') { + replyBuf.append('\r'); + } + } + replyBuf.append((char) c); + if (c == '\n') { + break; + } + } + response = replyBuf.toString(); + replyBuf.setLength(0); + if (logger.isLoggable(PlatformLogger.FINEST)) { + logger.finest("Server [" + serverAddr + "] --> " + response); + } + + if (response.length() == 0) { + code = -1; + } else { + try { + code = Integer.parseInt(response.substring(0, 3)); + } catch (NumberFormatException e) { + code = -1; + } catch (StringIndexOutOfBoundsException e) { + /* this line doesn't contain a response code, so + we just completely ignore it */ + continue; + } + } + serverResponse.addElement(response); + if (continuingCode != -1) { + /* we've seen a ###- sequence */ + if (code != continuingCode || + (response.length() >= 4 && response.charAt(3) == '-')) { + continue; + } else { + /* seen the end of code sequence */ + continuingCode = -1; + break; + } + } else if (response.length() >= 4 && response.charAt(3) == '-') { + continuingCode = code; + continue; + } else { + break; + } + } + + return code; + } + + /** Sends command cmd to the server. */ + private void sendServer(String cmd) { + out.print(cmd); + if (logger.isLoggable(PlatformLogger.FINEST)) { + logger.finest("Server [" + serverAddr + "] <-- " + cmd); + } + } + + /** converts the server response into a string. */ + private String getResponseString() { + return serverResponse.elementAt(0); + } + + /** Returns all server response strings. */ + private Vector getResponseStrings() { + return serverResponse; + } + + /** + * Read the reply from the FTP server. + * + * @return true if the command was successful + * @throws IOException if an error occured + */ + private boolean readReply() throws IOException { + lastReplyCode = FtpReplyCode.find(readServerResponse()); + + if (lastReplyCode.isPositivePreliminary()) { + replyPending = true; + return true; + } + if (lastReplyCode.isPositiveCompletion() || lastReplyCode.isPositiveIntermediate()) { + if (lastReplyCode == FtpReplyCode.CLOSING_DATA_CONNECTION) { + getTransferName(); + } + return true; + } + return false; + } + + /** + * Sends a command to the FTP server and returns the error code + * (which can be a "success") sent by the server. + * + * @param cmd + * @return true if the command was successful + * @throws IOException + */ + private boolean issueCommand(String cmd) throws IOException { + if (!isConnected()) { + throw new IllegalStateException("Not connected"); + } + if (replyPending) { + try { + completePending(); + } catch (sun.net.ftp.FtpProtocolException e) { + // ignore... + } + } + sendServer(cmd + "\r\n"); + return readReply(); + } + + /** + * Send a command to the FTP server and check for success. + * + * @param cmd String containing the command + * + * @throws FtpProtocolException if an error occured + */ + private void issueCommandCheck(String cmd) throws sun.net.ftp.FtpProtocolException, IOException { + if (!issueCommand(cmd)) { + throw new sun.net.ftp.FtpProtocolException(cmd + ":" + getResponseString(), getLastReplyCode()); + } + } + private static Pattern epsvPat = null; + private static Pattern pasvPat = null; + + /** + * Opens a "PASSIVE" connection with the server and returns the connected + * Socket. + * + * @return the connected Socket + * @throws IOException if the connection was unsuccessful. + */ + private Socket openPassiveDataConnection(String cmd) throws sun.net.ftp.FtpProtocolException, 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 command first. + * EPSV is documented in RFC2428 + * - If EPSV fails, then we fall back to the older, yet ok, PASV + * - 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")) { + // We can safely use EPSV commands + issueCommandCheck("EPSV"); + 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. + + if (epsvPat == null) { + epsvPat = Pattern.compile("^229 .* \\(\\|\\|\\|(\\d+)\\|\\)"); + } + Matcher m = epsvPat.matcher(serverAnswer); + if (!m.find()) { + throw new sun.net.ftp.FtpProtocolException("EPSV failed : " + serverAnswer); + } + // Yay! Let's extract the port number + String s = m.group(1); + port = Integer.parseInt(s); + InetAddress add = server.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(serverAddr.getHostName(), port); + } + } else { + // EPSV ALL failed, so Let's try the regular PASV cmd + issueCommandCheck("PASV"); + 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. + + if (pasvPat == null) { + pasvPat = Pattern.compile("227 .* \\(?(\\d{1,3},\\d{1,3},\\d{1,3},\\d{1,3}),(\\d{1,3}),(\\d{1,3})\\)?"); + } + Matcher m = pasvPat.matcher(serverAnswer); + if (!m.find()) { + throw new sun.net.ftp.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() { + + public Socket run() { + return new Socket(proxy); + } + }); + } else { + s = new Socket(Proxy.NO_PROXY); + } + } else { + s = new Socket(); + } + // Bind the socket to the same address as the control channel. This + // is needed in case of multi-homed systems. + s.bind(new InetSocketAddress(server.getLocalAddress(), 0)); + 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); + } + if (useCrypto) { + try { + s = sslFact.createSocket(s, dest.getHostName(), dest.getPort(), true); + } catch (Exception e) { + throw new sun.net.ftp.FtpProtocolException("Can't open secure data channel: " + e); + } + } + if (!issueCommand(cmd)) { + s.close(); + throw new sun.net.ftp.FtpProtocolException(cmd + ":" + getResponseString(), getLastReplyCode()); + } + return s; + } + + /** + * Opens a data connection with the server according to the set mode + * (ACTIVE or PASSIVE) then send the command passed as an argument. + * + * @param cmd the String containing the command to execute + * @return the connected Socket + * @throws IOException if the connection or command failed + */ + private Socket openDataConnection(String cmd) throws sun.net.ftp.FtpProtocolException, IOException { + Socket clientSocket; + + if (passiveMode) { + return openPassiveDataConnection(cmd); + } + ServerSocket portSocket; + InetAddress myAddress; + String portCmd; + + 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 sun.net.ftp.FtpProtocolException("Passive mode failed"); + } + // Bind the ServerSocket to the same address as the control channel + // This is needed for multi-homed systems + portSocket = new ServerSocket(0, 1, server.getLocalAddress()); + try { + myAddress = portSocket.getInetAddress(); + if (myAddress.isAnyLocalAddress()) { + myAddress = server.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) || !issueCommand(cmd)) { + // 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); + issueCommandCheck(portCmd); + issueCommandCheck(cmd); + } + // 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(); + } + if (useCrypto) { + try { + clientSocket = sslFact.createSocket(clientSocket, serverAddr.getHostName(), serverAddr.getPort(), true); + } catch (Exception ex) { + throw new IOException(ex.getLocalizedMessage()); + } + } + return clientSocket; + } + + private InputStream createInputStream(InputStream in) { + if (type == TransferType.ASCII) { + return new sun.net.TelnetInputStream(in, false); + } + return in; + } + + private OutputStream createOutputStream(OutputStream out) { + if (type == TransferType.ASCII) { + return new sun.net.TelnetOutputStream(out, false); + } + return out; + } + + /** + * Creates an instance of FtpClient. The client is not connected to any + * server yet. + * + */ + protected FtpClient() { + } + + /** + * Creates an instance of FtpClient. The client is not connected to any + * server yet. + * + */ + public static sun.net.ftp.FtpClient create() { + return new FtpClient(); + } + + /** + * Set the transfer mode to passive. In that mode, data connections + * are established by having the client connect to the server. + * This is the recommended default mode as it will work best through + * firewalls and NATs. + * + * @return This FtpClient + * @see #setActiveMode() + */ + public sun.net.ftp.FtpClient enablePassiveMode(boolean passive) { + passiveMode = passive; + return this; + } + + /** + * Gets the current transfer mode. + * + * @return the current FtpTransferMode + */ + public boolean isPassiveModeEnabled() { + return passiveMode; + } + + /** + * Sets the timeout value to use when connecting to the server, + * + * @param timeout the timeout value, in milliseconds, to use for the connect + * operation. A value of zero or less, means use the default timeout. + * + * @return This FtpClient + */ + public sun.net.ftp.FtpClient setConnectTimeout(int timeout) { + connectTimeout = timeout; + return this; + } + + /** + * Returns the current connection timeout value. + * + * @return the value, in milliseconds, of the current connect timeout. + * @see #setConnectTimeout(int) + */ + public int getConnectTimeout() { + return connectTimeout; + } + + /** + * Sets the timeout value to use when reading from the server, + * + * @param timeout the timeout value, in milliseconds, to use for the read + * operation. A value of zero or less, means use the default timeout. + * @return This FtpClient + */ + public sun.net.ftp.FtpClient setReadTimeout(int timeout) { + readTimeout = timeout; + return this; + } + + /** + * Returns the current read timeout value. + * + * @return the value, in milliseconds, of the current read timeout. + * @see #setReadTimeout(int) + */ + public int getReadTimeout() { + return readTimeout; + } + + public sun.net.ftp.FtpClient setProxy(Proxy p) { + proxy = p; + return this; + } + + /** + * Get the proxy of this FtpClient + * + * @return the Proxy, this client is using, or null + * if none is used. + * @see #setProxy(Proxy) + */ + public Proxy getProxy() { + return proxy; + } + + /** + * Connects to the specified destination. + * + * @param dest the InetSocketAddress to connect to. + * @throws IOException if the connection fails. + */ + private void tryConnect(InetSocketAddress dest, int timeout) throws IOException { + if (isConnected()) { + disconnect(); + } + server = doConnect(dest, timeout); + try { + out = new PrintStream(new BufferedOutputStream(server.getOutputStream()), + true, encoding); + } catch (UnsupportedEncodingException e) { + throw new InternalError(encoding + "encoding not found"); + } + in = new BufferedInputStream(server.getInputStream()); + } + + private Socket doConnect(InetSocketAddress dest, int timeout) throws IOException { + Socket s; + if (proxy != null) { + if (proxy.type() == Proxy.Type.SOCKS) { + s = AccessController.doPrivileged( + new PrivilegedAction() { + + public Socket run() { + return new Socket(proxy); + } + }); + } else { + s = new Socket(Proxy.NO_PROXY); + } + } else { + s = new Socket(); + } + // Instance specific timeouts do have priority, that means + // connectTimeout & readTimeout (-1 means not set) + // Then global default timeouts + // Then no timeout. + if (timeout >= 0) { + s.connect(dest, timeout); + } else { + 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; + } + + private void disconnect() throws IOException { + if (isConnected()) { + server.close(); + } + server = null; + in = null; + out = null; + lastTransSize = -1; + lastFileName = null; + restartOffset = 0; + welcomeMsg = null; + lastReplyCode = null; + serverResponse.setSize(0); + } + + /** + * Tests whether this client is connected or not to a server. + * + * @return true if the client is connected. + */ + public boolean isConnected() { + return server != null; + } + + public SocketAddress getServerAddress() { + return server == null ? null : server.getRemoteSocketAddress(); + } + + public sun.net.ftp.FtpClient connect(SocketAddress dest) throws sun.net.ftp.FtpProtocolException, IOException { + return connect(dest, -1); + } + + /** + * Connects the FtpClient to the specified destination. + * + * @param dest the address of the destination server + * @throws IOException if connection failed. + */ + public sun.net.ftp.FtpClient connect(SocketAddress dest, int timeout) throws sun.net.ftp.FtpProtocolException, IOException { + if (!(dest instanceof InetSocketAddress)) { + throw new IllegalArgumentException("Wrong address type"); + } + serverAddr = (InetSocketAddress) dest; + tryConnect(serverAddr, timeout); + if (!readReply()) { + throw new sun.net.ftp.FtpProtocolException("Welcome message: " + + getResponseString(), lastReplyCode); + } + welcomeMsg = getResponseString().substring(4); + return this; + } + + private void tryLogin(String user, char[] password) throws sun.net.ftp.FtpProtocolException, IOException { + issueCommandCheck("USER " + user); + + /* + * Checks for "331 User name okay, need password." answer + */ + if (lastReplyCode == FtpReplyCode.NEED_PASSWORD) { + if ((password != null) && (password.length > 0)) { + issueCommandCheck("PASS " + String.valueOf(password)); + } + } + } + + /** + * Attempts to log on the server with the specified user name and password. + * + * @param user The user name + * @param password The password for that user + * @return true if the login was successful. + * @throws IOException if an error occured during the transmission + */ + public sun.net.ftp.FtpClient login(String user, char[] password) throws sun.net.ftp.FtpProtocolException, IOException { + if (!isConnected()) { + throw new sun.net.ftp.FtpProtocolException("Not connected yet", FtpReplyCode.BAD_SEQUENCE); + } + if (user == null || user.length() == 0) { + throw new IllegalArgumentException("User name can't be null or empty"); + } + tryLogin(user, password); + + // 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 = 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; + return this; + } + + /** + * Attempts to log on the server with the specified user name, password and + * account name. + * + * @param user The user name + * @param password The password for that user. + * @param account The account name for that user. + * @return true if the login was successful. + * @throws IOException if an error occurs during the transmission. + */ + public sun.net.ftp.FtpClient login(String user, char[] password, String account) throws sun.net.ftp.FtpProtocolException, IOException { + + if (!isConnected()) { + throw new sun.net.ftp.FtpProtocolException("Not connected yet", FtpReplyCode.BAD_SEQUENCE); + } + if (user == null || user.length() == 0) { + throw new IllegalArgumentException("User name can't be null or empty"); + } + tryLogin(user, password); + + /* + * Checks for "332 Need account for login." answer + */ + if (lastReplyCode == FtpReplyCode.NEED_ACCOUNT) { + issueCommandCheck("ACCT " + account); + } + + // keep the welcome message around so we can + // put it in the resulting HTML page. + StringBuffer sb = new StringBuffer(); + if (serverResponse != null) { + for (String l : serverResponse) { + 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; + return this; + } + + /** + * Logs out the current user. This is in effect terminates the current + * session and the connection to the server will be closed. + * + */ + public void close() throws IOException { + if (isConnected()) { + issueCommand("QUIT"); + loggedIn = false; + } + disconnect(); + } + + /** + * Checks whether the client is logged in to the server or not. + * + * @return true if the client has already completed a login. + */ + public boolean isLoggedIn() { + return loggedIn; + } + + /** + * Changes to a specific directory on a remote FTP server + * + * @param remoteDirectory path of the directory to CD to. + * @return true if the operation was successful. + * @exception FtpProtocolException + */ + public sun.net.ftp.FtpClient changeDirectory(String remoteDirectory) throws sun.net.ftp.FtpProtocolException, IOException { + if (remoteDirectory == null || "".equals(remoteDirectory)) { + throw new IllegalArgumentException("directory can't be null or empty"); + } + + issueCommandCheck("CWD " + remoteDirectory); + return this; + } + + /** + * Changes to the parent directory, sending the CDUP command to the server. + * + * @return true if the command was successful. + * @throws IOException + */ + public sun.net.ftp.FtpClient changeToParentDirectory() throws sun.net.ftp.FtpProtocolException, IOException { + issueCommandCheck("CDUP"); + return this; + } + + /** + * Returns the server current working directory, or null if + * the PWD command failed. + * + * @return a String containing the current working directory, + * or null + * @throws IOException + */ + public String getWorkingDirectory() throws sun.net.ftp.FtpProtocolException, IOException { + issueCommandCheck("PWD"); + /* + * answer will be of the following format : + * + * 257 "/" is current directory. + */ + String answ = getResponseString(); + if (!answ.startsWith("257")) { + return null; + } + return answ.substring(5, answ.lastIndexOf('"')); + } + + /** + * Sets the restart offset to the specified value. That value will be + * sent through a REST command to server before a file + * transfer and has the effect of resuming a file transfer from the + * specified point. After a transfer the restart offset is set back to + * zero. + * + * @param offset the offset in the remote file at which to start the next + * transfer. This must be a value greater than or equal to zero. + * @throws IllegalArgumentException if the offset is negative. + */ + public sun.net.ftp.FtpClient setRestartOffset(long offset) { + if (offset < 0) { + throw new IllegalArgumentException("offset can't be negative"); + } + restartOffset = offset; + return this; + } + + /** + * Retrieves a file from the ftp server and writes it to the specified + * OutputStream. + * If the restart offset was set, then a REST command will be + * sent before the RETR in order to restart the tranfer from the specified + * offset. + * The OutputStream is not closed by this method at the end + * of the transfer. + * + * @param name a String containing the name of the file to + * retreive from the server. + * @param local the OutputStream the file should be written to. + * @throws IOException if the transfer fails. + */ + public sun.net.ftp.FtpClient getFile(String name, OutputStream local) throws sun.net.ftp.FtpProtocolException, IOException { + int mtu = 1500; + if (restartOffset > 0) { + Socket s; + try { + s = openDataConnection("REST " + restartOffset); + } finally { + restartOffset = 0; + } + issueCommandCheck("RETR " + name); + getTransferSize(); + InputStream remote = createInputStream(s.getInputStream()); + byte[] buf = new byte[mtu * 10]; + int l; + while ((l = remote.read(buf)) >= 0) { + if (l > 0) { + local.write(buf, 0, l); + } + } + remote.close(); + } else { + Socket s = openDataConnection("RETR " + name); + getTransferSize(); + InputStream remote = createInputStream(s.getInputStream()); + byte[] buf = new byte[mtu * 10]; + int l; + while ((l = remote.read(buf)) >= 0) { + if (l > 0) { + local.write(buf, 0, l); + } + } + remote.close(); + } + return completePending(); + } + + /** + * Retrieves a file from the ftp server, using the RETR command, and + * returns the InputStream from* the established data connection. + * {@link #completePending()} has to be called once the application + * is done reading from the returned stream. + * + * @param name the name of the remote file + * @return the {@link java.io.InputStream} from the data connection, or + * null if the command was unsuccessful. + * @throws IOException if an error occured during the transmission. + */ + public InputStream getFileStream(String name) throws sun.net.ftp.FtpProtocolException, IOException { + Socket s; + if (restartOffset > 0) { + try { + s = openDataConnection("REST " + restartOffset); + } finally { + restartOffset = 0; + } + if (s == null) { + return null; + } + issueCommandCheck("RETR " + name); + getTransferSize(); + return createInputStream(s.getInputStream()); + } + + s = openDataConnection("RETR " + name); + if (s == null) { + return null; + } + getTransferSize(); + return createInputStream(s.getInputStream()); + } + + /** + * Transfers a file from the client to the server (aka a put) + * by sending the STOR or STOU command, depending on the + * unique argument, and returns the OutputStream + * from the established data connection. + * {@link #completePending()} has to be called once the application + * is finished writing to the stream. + * + * A new file is created at the server site if the file specified does + * not already exist. + * + * If unique is set to true, the resultant file + * is to be created under a name unique to that directory, meaning + * it will not overwrite an existing file, instead the server will + * generate a new, unique, file name. + * The name of the remote file can be retrieved, after completion of the + * transfer, by calling {@link #getLastFileName()}. + * + * @param name the name of the remote file to write. + * @param unique true if the remote files should be unique, + * in which case the STOU command will be used. + * @return the {@link java.io.OutputStream} from the data connection or + * null if the command was unsuccessful. + * @throws IOException if an error occured during the transmission. + */ + public OutputStream putFileStream(String name, boolean unique) throws sun.net.ftp.FtpProtocolException, IOException { + String cmd = unique ? "STOU " : "STOR "; + Socket s = openDataConnection(cmd + name); + if (s == null) { + return null; + } + if (type == TransferType.BINARY) { + return s.getOutputStream(); + } + return new sun.net.TelnetOutputStream(s.getOutputStream(), false); + } + + /** + * Transfers a file from the client to the server (aka a put) + * by sending the STOR command. The content of the InputStream + * passed in argument is written into the remote file, overwriting any + * existing data. + * + * A new file is created at the server site if the file specified does + * not already exist. + * + * @param name the name of the remote file to write. + * @param local the InputStream that points to the data to + * transfer. + * @param unique true if the remote file should be unique + * (i.e. not already existing), false otherwise. + * @return true if the transfer was successful. + * @throws IOException if an error occured during the transmission. + * @see #getLastFileName() + */ + public sun.net.ftp.FtpClient putFile(String name, InputStream local, boolean unique) throws sun.net.ftp.FtpProtocolException, IOException { + String cmd = unique ? "STOU " : "STOR "; + int mtu = 1500; + if (type == TransferType.BINARY) { + Socket s = openDataConnection(cmd + name); + OutputStream remote = createOutputStream(s.getOutputStream()); + byte[] buf = new byte[mtu * 10]; + int l; + while ((l = local.read(buf)) >= 0) { + if (l > 0) { + remote.write(buf, 0, l); + } + } + remote.close(); + } + return completePending(); + } + + /** + * Sends the APPE command to the server in order to transfer a data stream + * passed in argument and append it to the content of the specified remote + * file. + * + * @param name A String containing the name of the remote file + * to append to. + * @param local The InputStream providing access to the data + * to be appended. + * @return true if the transfer was successful. + * @throws IOException if an error occured during the transmission. + */ + public sun.net.ftp.FtpClient appendFile(String name, InputStream local) throws sun.net.ftp.FtpProtocolException, IOException { + int mtu = 1500; + Socket s = openDataConnection("APPE " + name); + OutputStream remote = createOutputStream(s.getOutputStream()); + byte[] buf = new byte[mtu * 10]; + int l; + while ((l = local.read(buf)) >= 0) { + if (l > 0) { + remote.write(buf, 0, l); + } + } + remote.close(); + return completePending(); + } + + /** + * Renames a file on the server. + * + * @param from the name of the file being renamed + * @param to the new name for the file + * @throws IOException if the command fails + */ + public sun.net.ftp.FtpClient rename(String from, String to) throws sun.net.ftp.FtpProtocolException, IOException { + issueCommandCheck("RNFR " + from); + issueCommandCheck("RNTO " + to); + return this; + } + + /** + * Deletes a file on the server. + * + * @param name a String containing the name of the file + * to delete. + * @return true if the command was successful + * @throws IOException if an error occured during the exchange + */ + public sun.net.ftp.FtpClient deleteFile(String name) throws sun.net.ftp.FtpProtocolException, IOException { + issueCommandCheck("DELE " + name); + return this; + } + + /** + * Creates a new directory on the server. + * + * @param name a String containing the name of the directory + * to create. + * @return true if the operation was successful. + * @throws IOException if an error occured during the exchange + */ + public sun.net.ftp.FtpClient makeDirectory(String name) throws sun.net.ftp.FtpProtocolException, IOException { + issueCommandCheck("MKD " + name); + return this; + } + + /** + * Removes a directory on the server. + * + * @param name a String containing the name of the directory + * to remove. + * + * @return true if the operation was successful. + * @throws IOException if an error occured during the exchange. + */ + public sun.net.ftp.FtpClient removeDirectory(String name) throws sun.net.ftp.FtpProtocolException, IOException { + issueCommandCheck("RMD " + name); + return this; + } + + /** + * Sends a No-operation command. It's useful for testing the connection + * status or as a keep alive mechanism. + * + * @throws FtpProtocolException if the command fails + */ + public sun.net.ftp.FtpClient noop() throws sun.net.ftp.FtpProtocolException, IOException { + issueCommandCheck("NOOP"); + return this; + } + + /** + * Sends the STAT command to the server. + * This can be used while a data connection is open to get a status + * on the current transfer, in that case the parameter should be + * null. + * If used between file transfers, it may have a pathname as argument + * in which case it will work as the LIST command except no data + * connection will be created. + * + * @param name an optional String containing the pathname + * the STAT command should apply to. + * @return the response from the server or null if the + * command failed. + * @throws IOException if an error occured during the transmission. + */ + public String getStatus(String name) throws sun.net.ftp.FtpProtocolException, IOException { + issueCommandCheck((name == null ? "STAT" : "STAT " + name)); + /* + * A typical response will be: + * 213-status of t32.gif: + * -rw-r--r-- 1 jcc staff 247445 Feb 17 1998 t32.gif + * 213 End of Status + * + * or + * + * 211-jsn FTP server status: + * Version wu-2.6.2+Sun + * Connected to localhost (::1) + * Logged in as jccollet + * TYPE: ASCII, FORM: Nonprint; STRUcture: File; transfer MODE: Stream + * No data connection + * 0 data bytes received in 0 files + * 0 data bytes transmitted in 0 files + * 0 data bytes total in 0 files + * 53 traffic bytes received in 0 transfers + * 485 traffic bytes transmitted in 0 transfers + * 587 traffic bytes total in 0 transfers + * 211 End of status + * + * So we need to remove the 1st and last line + */ + Vector resp = getResponseStrings(); + StringBuffer sb = new StringBuffer(); + for (int i = 1; i < resp.size() - 1; i++) { + sb.append(resp.get(i)); + } + return sb.toString(); + } + + /** + * Sends the FEAT command to the server and returns the list of supported + * features in the form of strings. + * + * The features are the supported commands, like AUTH TLS, PROT or PASV. + * See the RFCs for a complete list. + * + * Note that not all FTP servers support that command, in which case + * the method will return null + * + * @return a List of Strings describing the + * supported additional features, or null + * if the command is not supported. + * @throws IOException if an error occurs during the transmission. + */ + public List getFeatures() throws sun.net.ftp.FtpProtocolException, IOException { + /* + * The FEAT command, when implemented will return something like: + * + * 211-Features: + * AUTH TLS + * PBSZ + * PROT + * EPSV + * EPRT + * PASV + * REST STREAM + * 211 END + */ + ArrayList features = new ArrayList(); + issueCommandCheck("FEAT"); + Vector resp = getResponseStrings(); + // Note that we start at index 1 to skip the 1st line (211-...) + // and we stop before the last line. + for (int i = 1; i < resp.size() - 1; i++) { + String s = resp.get(i); + // Get rid of leading space and trailing newline + features.add(s.substring(1, s.length() - 1)); + } + return features; + } + + /** + * sends the ABOR command to the server. + * It tells the server to stop the previous command or transfer. + * + * @return true if the command was successful. + * @throws IOException if an error occured during the transmission. + */ + public sun.net.ftp.FtpClient abort() throws sun.net.ftp.FtpProtocolException, IOException { + issueCommandCheck("ABOR"); + // TODO: Must check the ReplyCode: + /* + * From the RFC: + * There are two cases for the server upon receipt of this + * command: (1) the FTP service command was already completed, + * or (2) the FTP service command is still in progress. + * In the first case, the server closes the data connection + * (if it is open) and responds with a 226 reply, indicating + * that the abort command was successfully processed. + * In the second case, the server aborts the FTP service in + * progress and closes the data connection, returning a 426 + * reply to indicate that the service request terminated + * abnormally. The server then sends a 226 reply, + * indicating that the abort command was successfully + * processed. + */ + + + return this; + } + + /** + * Some methods do not wait until completion before returning, so this + * method can be called to wait until completion. This is typically the case + * with commands that trigger a transfer like {@link #getFileStream(String)}. + * So this method should be called before accessing information related to + * such a command. + *

This method will actually block reading on the command channel for a + * notification from the server that the command is finished. Such a + * notification often carries extra information concerning the completion + * of the pending action (e.g. number of bytes transfered).

+ *

Note that this will return true immediately if no command or action + * is pending

+ *

It should be also noted that most methods issuing commands to the ftp + * server will call this method if a previous command is pending. + *

Example of use: + *

+     * InputStream in = cl.getFileStream("file");
+     * ...
+     * cl.completePending();
+     * long size = cl.getLastTransferSize();
+     * 
+ * On the other hand, it's not necessary in a case like: + *
+     * InputStream in = cl.getFileStream("file");
+     * // read content
+     * ...
+     * cl.logout();
+     * 
+ *

Since {@link #logout()} will call completePending() if necessary.

+ * @return true if the completion was successful or if no + * action was pending. + * @throws IOException + */ + public sun.net.ftp.FtpClient completePending() throws sun.net.ftp.FtpProtocolException, IOException { + while (replyPending) { + replyPending = false; + if (!readReply()) { + throw new sun.net.ftp.FtpProtocolException(getLastResponseString(), lastReplyCode); + } + } + return this; + } + + /** + * Reinitializes the USER parameters on the FTP server + * + * @throws FtpProtocolException if the command fails + */ + public sun.net.ftp.FtpClient reInit() throws sun.net.ftp.FtpProtocolException, IOException { + issueCommandCheck("REIN"); + loggedIn = false; + if (useCrypto) { + if (server instanceof SSLSocket) { + javax.net.ssl.SSLSession session = ((SSLSocket) server).getSession(); + session.invalidate(); + // Restore previous socket and streams + server = oldSocket; + oldSocket = null; + try { + out = new PrintStream(new BufferedOutputStream(server.getOutputStream()), + true, encoding); + } catch (UnsupportedEncodingException e) { + throw new InternalError(encoding + "encoding not found"); + } + in = new BufferedInputStream(server.getInputStream()); + } + } + useCrypto = false; + return this; + } + + /** + * Changes the transfer type (binary, ascii, ebcdic) and issue the + * proper command (e.g. TYPE A) to the server. + * + * @param type the FtpTransferType to use. + * @return This FtpClient + * @throws IOException if an error occurs during transmission. + */ + public sun.net.ftp.FtpClient setType(TransferType type) throws sun.net.ftp.FtpProtocolException, IOException { + String cmd = "NOOP"; + + this.type = type; + if (type == TransferType.ASCII) { + cmd = "TYPE A"; + } + if (type == TransferType.BINARY) { + cmd = "TYPE I"; + } + if (type == TransferType.EBCDIC) { + cmd = "TYPE E"; + } + issueCommandCheck(cmd); + return this; + } + + /** + * Issues a LIST command to the server to get the current directory + * listing, and returns the InputStream from the data connection. + * {@link #completePending()} has to be called once the application + * is finished writing to the stream. + * + * @param path the pathname of the directory to list, or null + * for the current working directory. + * @return the InputStream from the resulting data connection + * @throws IOException if an error occurs during the transmission. + * @see #changeDirectory(String) + * @see #listFiles(String) + */ + public InputStream list(String path) throws sun.net.ftp.FtpProtocolException, IOException { + Socket s; + s = openDataConnection(path == null ? "LIST" : "LIST " + path); + if (s != null) { + return createInputStream(s.getInputStream()); + } + return null; + } + + /** + * Issues a NLST path command to server to get the specified directory + * content. It differs from {@link #list(String)} method by the fact that + * it will only list the file names which would make the parsing of the + * somewhat easier. + * + * {@link #completePending()} has to be called once the application + * is finished writing to the stream. + * + * @param path a String containing the pathname of the + * directory to list or null for the current working + * directory. + * @return the InputStream from the resulting data connection + * @throws IOException if an error occurs during the transmission. + */ + public InputStream nameList(String path) throws sun.net.ftp.FtpProtocolException, IOException { + Socket s; + s = openDataConnection("NLST " + path); + if (s != null) { + return createInputStream(s.getInputStream()); + } + return null; + } + + /** + * Issues the SIZE [path] command to the server to get the size of a + * specific file on the server. + * Note that this command may not be supported by the server. In which + * case -1 will be returned. + * + * @param path a String containing the pathname of the + * file. + * @return a long containing the size of the file or -1 if + * the server returned an error, which can be checked with + * {@link #getLastReplyCode()}. + * @throws IOException if an error occurs during the transmission. + */ + public long getSize(String path) throws sun.net.ftp.FtpProtocolException, IOException { + if (path == null || path.length() == 0) { + throw new IllegalArgumentException("path can't be null or empty"); + } + issueCommandCheck("SIZE " + path); + if (lastReplyCode == FtpReplyCode.FILE_STATUS) { + String s = getResponseString(); + s = s.substring(4, s.length() - 1); + return Long.parseLong(s); + } + return -1; + } + private static String[] MDTMformats = { + "yyyyMMddHHmmss.SSS", + "yyyyMMddHHmmss" + }; + private static SimpleDateFormat[] dateFormats = new SimpleDateFormat[MDTMformats.length]; + + static { + for (int i = 0; i < MDTMformats.length; i++) { + dateFormats[i] = new SimpleDateFormat(MDTMformats[i]); + dateFormats[i].setTimeZone(TimeZone.getTimeZone("GMT")); + } + } + + /** + * Issues the MDTM [path] command to the server to get the modification + * time of a specific file on the server. + * Note that this command may not be supported by the server, in which + * case null will be returned. + * + * @param path a String containing the pathname of the file. + * @return a Date representing the last modification time + * or null if the server returned an error, which + * can be checked with {@link #getLastReplyCode()}. + * @throws IOException if an error occurs during the transmission. + */ + public Date getLastModified(String path) throws sun.net.ftp.FtpProtocolException, IOException { + issueCommandCheck("MDTM " + path); + if (lastReplyCode == FtpReplyCode.FILE_STATUS) { + String s = getResponseString().substring(4); + Date d = null; + for (SimpleDateFormat dateFormat : dateFormats) { + try { + d = dateFormat.parse(s); + } catch (ParseException ex) { + } + if (d != null) { + return d; + } + } + } + return null; + } + + /** + * Sets the parser used to handle the directory output to the specified + * one. By default the parser is set to one that can handle most FTP + * servers output (Unix base mostly). However it may be necessary for + * and application to provide its own parser due to some uncommon + * output format. + * + * @param p The FtpDirParser to use. + * @see #listFiles(String) + */ + public sun.net.ftp.FtpClient setDirParser(FtpDirParser p) { + parser = p; + return this; + } + + private class FtpFileIterator implements Iterator, Closeable { + + private BufferedReader in = null; + private FtpDirEntry nextFile = null; + private FtpDirParser fparser = null; + private boolean eof = false; + + public FtpFileIterator(FtpDirParser p, BufferedReader in) { + this.in = in; + this.fparser = p; + readNext(); + } + + private void readNext() { + nextFile = null; + if (eof) { + return; + } + String line = null; + try { + do { + line = in.readLine(); + if (line != null) { + nextFile = fparser.parseLine(line); + if (nextFile != null) { + return; + } + } + } while (line != null); + in.close(); + } catch (IOException iOException) { + } + eof = true; + } + + public boolean hasNext() { + return nextFile != null; + } + + public FtpDirEntry next() { + FtpDirEntry ret = nextFile; + readNext(); + return ret; + } + + public void remove() { + throw new UnsupportedOperationException("Not supported yet."); + } + + public void close() throws IOException { + if (in != null && !eof) { + in.close(); + } + eof = true; + nextFile = null; + } + } + + /** + * Issues a MLSD command to the server to get the specified directory + * listing and applies the current parser to create an Iterator of + * {@link java.net.ftp.FtpDirEntry}. Note that the Iterator returned is also a + * {@link java.io.Closeable}. + * If the server doesn't support the MLSD command, the LIST command is used + * instead. + * + * {@link #completePending()} has to be called once the application + * is finished iterating through the files. + * + * @param path the pathname of the directory to list or null + * for the current working directoty. + * @return a Iterator of files or null if the + * command failed. + * @throws IOException if an error occured during the transmission + * @see #setDirParser(FtpDirParser) + * @see #changeDirectory(String) + */ + public Iterator listFiles(String path) throws sun.net.ftp.FtpProtocolException, IOException { + Socket s = null; + BufferedReader sin = null; + try { + s = openDataConnection(path == null ? "MLSD" : "MLSD " + path); + } catch (sun.net.ftp.FtpProtocolException FtpException) { + // The server doesn't understand new MLSD command, ignore and fall + // back to LIST + } + + if (s != null) { + sin = new BufferedReader(new InputStreamReader(s.getInputStream())); + return new FtpFileIterator(mlsxParser, sin); + } else { + s = openDataConnection(path == null ? "LIST" : "LIST " + path); + if (s != null) { + sin = new BufferedReader(new InputStreamReader(s.getInputStream())); + return new FtpFileIterator(parser, sin); + } + } + return null; + } + + private boolean sendSecurityData(byte[] buf) throws IOException { + BASE64Encoder encoder = new BASE64Encoder(); + String s = encoder.encode(buf); + return issueCommand("ADAT " + s); + } + + private byte[] getSecurityData() { + String s = getLastResponseString(); + if (s.substring(4, 9).equalsIgnoreCase("ADAT=")) { + BASE64Decoder decoder = new BASE64Decoder(); + try { + // Need to get rid of the leading '315 ADAT=' + // and the trailing newline + return decoder.decodeBuffer(s.substring(9, s.length() - 1)); + } catch (IOException e) { + // + } + } + return null; + } + + /** + * Attempts to use Kerberos GSSAPI as an authentication mechanism with the + * ftp server. This will issue an AUTH GSSAPI command, and if + * it is accepted by the server, will followup with ADAT + * command to exchange the various tokens until authentification is + * successful. This conforms to Appendix I of RFC 2228. + * + * @return true if authentication was successful. + * @throws IOException if an error occurs during the transmission. + */ + public sun.net.ftp.FtpClient useKerberos() throws sun.net.ftp.FtpProtocolException, IOException { + /* + * Comment out for the moment since it's not in use and would create + * needless cross-package links. + * + issueCommandCheck("AUTH GSSAPI"); + if (lastReplyCode != FtpReplyCode.NEED_ADAT) + throw new sun.net.ftp.FtpProtocolException("Unexpected reply from server"); + try { + GSSManager manager = GSSManager.getInstance(); + GSSName name = manager.createName("SERVICE:ftp@"+ + serverAddr.getHostName(), null); + GSSContext context = manager.createContext(name, null, null, + GSSContext.DEFAULT_LIFETIME); + context.requestMutualAuth(true); + context.requestReplayDet(true); + context.requestSequenceDet(true); + context.requestCredDeleg(true); + byte []inToken = new byte[0]; + while (!context.isEstablished()) { + byte[] outToken + = context.initSecContext(inToken, 0, inToken.length); + // send the output token if generated + if (outToken != null) { + if (sendSecurityData(outToken)) { + inToken = getSecurityData(); + } + } + } + loggedIn = true; + } catch (GSSException e) { + + } + */ + return this; + } + + /** + * Returns the Welcome string the server sent during initial connection. + * + * @return a String containing the message the server + * returned during connection or null. + */ + public String getWelcomeMsg() { + return welcomeMsg; + } + + /** + * Returns the last reply code sent by the server. + * + * @return the lastReplyCode + */ + public FtpReplyCode getLastReplyCode() { + return lastReplyCode; + } + + /** + * Returns the last response string sent by the server. + * + * @return the message string, which can be quite long, last returned + * by the server. + */ + public String getLastResponseString() { + StringBuffer sb = new StringBuffer(); + if (serverResponse != null) { + for (String l : serverResponse) { + if (l != null) { + sb.append(l); + } + } + } + return sb.toString(); + } + + /** + * Returns, when available, the size of the latest started transfer. + * This is retreived by parsing the response string received as an initial + * response to a RETR or similar request. + * + * @return the size of the latest transfer or -1 if either there was no + * transfer or the information was unavailable. + */ + public long getLastTransferSize() { + return lastTransSize; + } + + /** + * Returns, when available, the remote name of the last transfered file. + * This is mainly useful for "put" operation when the unique flag was + * set since it allows to recover the unique file name created on the + * server which may be different from the one submitted with the command. + * + * @return the name the latest transfered file remote name, or + * null if that information is unavailable. + */ + public String getLastFileName() { + return lastFileName; + } + + /** + * Attempts to switch to a secure, encrypted connection. This is done by + * sending the "AUTH TLS" command. + *

See RFC 4217

+ * If successful this will establish a secure command channel with the + * server, it will also make it so that all other transfers (e.g. a RETR + * command) will be done over an encrypted channel as well unless a + * {@link #reInit()} command or a {@link #endSecureSession()} command is issued. + * + * @return true if the operation was successful. + * @throws IOException if an error occured during the transmission. + * @see #endSecureSession() + */ + public sun.net.ftp.FtpClient startSecureSession() throws sun.net.ftp.FtpProtocolException, IOException { + if (!isConnected()) { + throw new sun.net.ftp.FtpProtocolException("Not connected yet", FtpReplyCode.BAD_SEQUENCE); + } + if (sslFact == null) { + try { + sslFact = (SSLSocketFactory) SSLSocketFactory.getDefault(); + } catch (Exception e) { + throw new IOException(e.getLocalizedMessage()); + } + } + issueCommandCheck("AUTH TLS"); + Socket s = null; + try { + s = sslFact.createSocket(server, serverAddr.getHostName(), serverAddr.getPort(), true); + } catch (javax.net.ssl.SSLException ssle) { + try { + disconnect(); + } catch (Exception e) { + } + throw ssle; + } + // Remember underlying socket so we can restore it later + oldSocket = server; + server = s; + try { + out = new PrintStream(new BufferedOutputStream(server.getOutputStream()), + true, encoding); + } catch (UnsupportedEncodingException e) { + throw new InternalError(encoding + "encoding not found"); + } + in = new BufferedInputStream(server.getInputStream()); + + issueCommandCheck("PBSZ 0"); + issueCommandCheck("PROT P"); + useCrypto = true; + return this; + } + + /** + * Sends a CCC command followed by a PROT C + * command to the server terminating an encrypted session and reverting + * back to a non crypted transmission. + * + * @return true if the operation was successful. + * @throws IOException if an error occured during transmission. + * @see #startSecureSession() + */ + public sun.net.ftp.FtpClient endSecureSession() throws sun.net.ftp.FtpProtocolException, IOException { + if (!useCrypto) { + return this; + } + + issueCommandCheck("CCC"); + issueCommandCheck("PROT C"); + useCrypto = false; + // Restore previous socket and streams + server = oldSocket; + oldSocket = null; + try { + out = new PrintStream(new BufferedOutputStream(server.getOutputStream()), + true, encoding); + } catch (UnsupportedEncodingException e) { + throw new InternalError(encoding + "encoding not found"); + } + in = new BufferedInputStream(server.getInputStream()); + + return this; + } + + /** + * Sends the "Allocate" (ALLO) command to the server telling it to + * pre-allocate the specified number of bytes for the next transfer. + * + * @param size The number of bytes to allocate. + * @return true if the operation was successful. + * @throws IOException if an error occured during the transmission. + */ + public sun.net.ftp.FtpClient allocate(long size) throws sun.net.ftp.FtpProtocolException, IOException { + issueCommandCheck("ALLO " + size); + return this; + } + + /** + * Sends the "Structure Mount" (SMNT) command to the server. This let the + * user mount a different file system data structure without altering his + * login or accounting information. + * + * @param struct a String containing the name of the + * structure to mount. + * @return true if the operation was successful. + * @throws IOException if an error occured during the transmission. + */ + public sun.net.ftp.FtpClient structureMount(String struct) throws sun.net.ftp.FtpProtocolException, IOException { + issueCommandCheck("SMNT " + struct); + return this; + } + + /** + * Sends a SYST (System) command to the server and returns the String + * sent back by the server describing the operating system at the + * server. + * + * @return a String describing the OS, or null + * if the operation was not successful. + * @throws IOException if an error occured during the transmission. + */ + public String getSystem() throws sun.net.ftp.FtpProtocolException, IOException { + issueCommandCheck("SYST"); + /* + * 215 UNIX Type: L8 Version: SUNOS + */ + String resp = getResponseString(); + // Get rid of the leading code and blank + return resp.substring(4); + } + + /** + * Sends the HELP command to the server, with an optional command, like + * SITE, and returns the text sent back by the server. + * + * @param cmd the command for which the help is requested or + * null for the general help + * @return a String containing the text sent back by the + * server, or null if the command failed. + * @throws IOException if an error occured during transmission + */ + public String getHelp(String cmd) throws sun.net.ftp.FtpProtocolException, IOException { + issueCommandCheck("HELP " + cmd); + /** + * + * HELP + * 214-The following commands are implemented. + * USER EPRT STRU ALLO DELE SYST RMD MDTM ADAT + * PASS EPSV MODE REST CWD STAT PWD PROT + * QUIT LPRT RETR RNFR LIST HELP CDUP PBSZ + * PORT LPSV STOR RNTO NLST NOOP STOU AUTH + * PASV TYPE APPE ABOR SITE MKD SIZE CCC + * 214 Direct comments to ftp-bugs@jsn. + * + * HELP SITE + * 214-The following SITE commands are implemented. + * UMASK HELP GROUPS + * IDLE ALIAS CHECKMETHOD + * CHMOD CDPATH CHECKSUM + * 214 Direct comments to ftp-bugs@jsn. + */ + Vector resp = getResponseStrings(); + if (resp.size() == 1) { + // Single line response + return resp.get(0).substring(4); + } + // on multiple lines answers, like the ones above, remove 1st and last + // line, concat the the others. + StringBuffer sb = new StringBuffer(); + for (int i = 1; i < resp.size() - 1; i++) { + sb.append(resp.get(i).substring(3)); + } + return sb.toString(); + } + + /** + * Sends the SITE command to the server. This is used by the server + * to provide services specific to his system that are essential + * to file transfer. + * + * @param cmd the command to be sent. + * @return true if the command was successful. + * @throws IOException if an error occured during transmission + */ + public sun.net.ftp.FtpClient siteCmd(String cmd) throws sun.net.ftp.FtpProtocolException, IOException { + issueCommandCheck("SITE " + cmd); + return this; + } +} diff --git a/jdk/src/share/classes/sun/net/www/protocol/ftp/FtpURLConnection.java b/jdk/src/share/classes/sun/net/www/protocol/ftp/FtpURLConnection.java index 91d3152190..f223eed584 100644 --- a/jdk/src/share/classes/sun/net/www/protocol/ftp/FtpURLConnection.java +++ b/jdk/src/share/classes/sun/net/www/protocol/ftp/FtpURLConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 1994-2008 Sun Microsystems, Inc. All Rights Reserved. + * Copyright 1994-2009 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 @@ -37,10 +37,8 @@ import java.io.FilterInputStream; import java.io.FilterOutputStream; import java.io.FileNotFoundException; import java.net.URL; -import java.net.URLStreamHandler; import java.net.SocketPermission; import java.net.UnknownHostException; -import java.net.MalformedURLException; import java.net.InetSocketAddress; import java.net.URI; import java.net.Proxy; @@ -84,7 +82,6 @@ public class FtpURLConnection extends URLConnection { // In case we have to use proxies, we use HttpURLConnection HttpURLConnection http = null; private Proxy instProxy; - Proxy proxy = null; InputStream is = null; OutputStream os = null; @@ -125,12 +122,11 @@ public class FtpURLConnection extends URLConnection { ftp = cl; } + @Override public void close() throws IOException { super.close(); - try { - if (ftp != null) - ftp.closeServer(); - } catch (IOException ex) { + if (ftp != null) { + ftp.close(); } } } @@ -149,12 +145,11 @@ public class FtpURLConnection extends URLConnection { ftp = cl; } + @Override public void close() throws IOException { super.close(); - try { - if (ftp != null) - ftp.closeServer(); - } catch (IOException ex) { + if (ftp != null) { + ftp.close(); } } } @@ -192,10 +187,12 @@ public class FtpURLConnection extends URLConnection { private void setTimeouts() { if (ftp != null) { - if (connectTimeout >= 0) + if (connectTimeout >= 0) { ftp.setConnectTimeout(connectTimeout); - if (readTimeout >= 0) + } + if (readTimeout >= 0) { ftp.setReadTimeout(readTimeout); + } } } @@ -218,21 +215,22 @@ public class FtpURLConnection extends URLConnection { * Do we have to use a proxy? */ ProxySelector sel = java.security.AccessController.doPrivileged( - new java.security.PrivilegedAction() { - public ProxySelector run() { - return ProxySelector.getDefault(); - } - }); + new java.security.PrivilegedAction() { + public ProxySelector run() { + return ProxySelector.getDefault(); + } + }); if (sel != null) { URI uri = sun.net.www.ParseUtil.toURI(url); Iterator it = sel.select(uri).iterator(); while (it.hasNext()) { p = it.next(); if (p == null || p == Proxy.NO_PROXY || - p.type() == Proxy.Type.SOCKS) + p.type() == Proxy.Type.SOCKS) { break; + } if (p.type() != Proxy.Type.HTTP || - !(p.address() instanceof InetSocketAddress)) { + !(p.address() instanceof InetSocketAddress)) { sel.connectFailed(uri, p.address(), new IOException("Wrong proxy type")); continue; } @@ -240,10 +238,14 @@ public class FtpURLConnection extends URLConnection { InetSocketAddress paddr = (InetSocketAddress) p.address(); try { http = new HttpURLConnection(url, p); - if (connectTimeout >= 0) + http.setDoInput(getDoInput()); + http.setDoOutput(getDoOutput()); + if (connectTimeout >= 0) { http.setConnectTimeout(connectTimeout); - if (readTimeout >= 0) + } + if (readTimeout >= 0) { http.setReadTimeout(readTimeout); + } http.connect(); connected = true; return; @@ -257,10 +259,14 @@ public class FtpURLConnection extends URLConnection { p = instProxy; if (p.type() == Proxy.Type.HTTP) { http = new HttpURLConnection(url, instProxy); - if (connectTimeout >= 0) + http.setDoInput(getDoInput()); + http.setDoOutput(getDoOutput()); + if (connectTimeout >= 0) { http.setConnectTimeout(connectTimeout); - if (readTimeout >= 0) + } + if (readTimeout >= 0) { http.setReadTimeout(readTimeout); + } http.connect(); connected = true; return; @@ -270,31 +276,35 @@ public class FtpURLConnection extends URLConnection { if (user == null) { user = "anonymous"; String vers = java.security.AccessController.doPrivileged( - new GetPropertyAction("java.version")); + new GetPropertyAction("java.version")); password = java.security.AccessController.doPrivileged( - new GetPropertyAction("ftp.protocol.user", - "Java" + vers +"@")); + new GetPropertyAction("ftp.protocol.user", + "Java" + vers + "@")); } try { - if (p != null) - ftp = new FtpClient(p); - else - ftp = new FtpClient(); + ftp = FtpClient.create(); + if (p != null) { + ftp.setProxy(p); + } setTimeouts(); - if (port != -1) - ftp.openServer(host, port); - else - ftp.openServer(host); + if (port != -1) { + ftp.connect(new InetSocketAddress(host, port)); + } else { + ftp.connect(new InetSocketAddress(host, FtpClient.defaultPort())); + } } catch (UnknownHostException e) { // Maybe do something smart here, like use a proxy like iftp. // Just keep throwing for now. throw e; + } catch (FtpProtocolException fe) { + throw new IOException(fe); } try { - ftp.login(user, password); - } catch (sun.net.ftp.FtpLoginException e) { - ftp.closeServer(); - throw e; + ftp.login(user, password.toCharArray()); + } catch (sun.net.ftp.FtpProtocolException e) { + ftp.close(); + // Backward compatibility + throw new sun.net.ftp.FtpLoginException("Invalid username/password"); } connected = true; } @@ -306,24 +316,29 @@ public class FtpURLConnection extends URLConnection { private void decodePath(String path) { int i = path.indexOf(";type="); if (i >= 0) { - String s1 = path.substring(i+6, path.length()); - if ("i".equalsIgnoreCase(s1)) + String s1 = path.substring(i + 6, path.length()); + if ("i".equalsIgnoreCase(s1)) { type = BIN; - if ("a".equalsIgnoreCase(s1)) + } + if ("a".equalsIgnoreCase(s1)) { type = ASCII; - if ("d".equalsIgnoreCase(s1)) + } + if ("d".equalsIgnoreCase(s1)) { type = DIR; + } path = path.substring(0, i); } if (path != null && path.length() > 1 && - path.charAt(0) == '/') + path.charAt(0) == '/') { path = path.substring(1); - if (path == null || path.length() == 0) + } + if (path == null || path.length() == 0) { path = "./"; + } if (!path.endsWith("/")) { i = path.lastIndexOf('/'); if (i > 0) { - filename = path.substring(i+1, path.length()); + filename = path.substring(i + 1, path.length()); filename = ParseUtil.decode(filename); pathname = path.substring(0, i); } else { @@ -334,10 +349,11 @@ public class FtpURLConnection extends URLConnection { pathname = path.substring(0, path.length() - 1); filename = null; } - if (pathname != null) + if (pathname != null) { fullpath = pathname + "/" + (filename != null ? filename : ""); - else + } else { fullpath = filename; + } } /* @@ -346,18 +362,19 @@ public class FtpURLConnection extends URLConnection { * This is because, '/' is not necessarly the directory delimiter * on every systems. */ - - private void cd(String path) throws IOException { - if (path == null || "".equals(path)) + private void cd(String path) throws FtpProtocolException, IOException { + if (path == null || path.isEmpty()) { return; + } if (path.indexOf('/') == -1) { - ftp.cd(ParseUtil.decode(path)); + ftp.changeDirectory(ParseUtil.decode(path)); return; } - StringTokenizer token = new StringTokenizer(path,"/"); - while (token.hasMoreTokens()) - ftp.cd(ParseUtil.decode(token.nextToken())); + StringTokenizer token = new StringTokenizer(path, "/"); + while (token.hasMoreTokens()) { + ftp.changeDirectory(ParseUtil.decode(token.nextToken())); + } } /** @@ -369,16 +386,19 @@ public class FtpURLConnection extends URLConnection { * @throws IOException if already opened for output * @throws FtpProtocolException if errors occur during the transfert. */ + @Override public InputStream getInputStream() throws IOException { if (!connected) { connect(); } - if (http != null) + if (http != null) { return http.getInputStream(); + } - if (os != null) + if (os != null) { throw new IOException("Already opened for output"); + } if (is != null) { return is; @@ -386,82 +406,85 @@ public class FtpURLConnection extends URLConnection { MessageHeader msgh = new MessageHeader(); + boolean isAdir = false; try { decodePath(url.getPath()); if (filename == null || type == DIR) { - ftp.ascii(); + ftp.setAsciiType(); cd(pathname); - if (filename == null) - is = new FtpInputStream(ftp, ftp.list()); - else + if (filename == null) { + is = new FtpInputStream(ftp, ftp.list(null)); + } else { is = new FtpInputStream(ftp, ftp.nameList(filename)); + } } else { - if (type == ASCII) - ftp.ascii(); - else - ftp.binary(); + if (type == ASCII) { + ftp.setAsciiType(); + } else { + ftp.setBinaryType(); + } cd(pathname); - is = new FtpInputStream(ftp, ftp.get(filename)); + is = new FtpInputStream(ftp, ftp.getFileStream(filename)); } /* Try to get the size of the file in bytes. If that is - successful, then create a MeteredStream. */ + successful, then create a MeteredStream. */ try { - String response = ftp.getResponseString(); - int offset; - - if ((offset = response.indexOf(" bytes)")) != -1) { - int i = offset; - int c; - - while (--i >= 0 && ((c = response.charAt(i)) >= '0' - && c <= '9')) - ; - long l = Long.parseLong(response.substring(i + 1, offset)); - msgh.add("content-length", Long.toString(l)); - if (l > 0) { - - // Wrap input stream with MeteredStream to ensure read() will always return -1 - // at expected length. - - // Check if URL should be metered - boolean meteredInput = ProgressMonitor.getDefault().shouldMeterInput(url, "GET"); - ProgressSource pi = null; - - if (meteredInput) { - pi = new ProgressSource(url, "GET", l); - pi.beginTracking(); - } + long l = ftp.getLastTransferSize(); + msgh.add("content-length", Long.toString(l)); + if (l > 0) { + + // Wrap input stream with MeteredStream to ensure read() will always return -1 + // at expected length. - is = new MeteredStream(is, pi, l); + // Check if URL should be metered + boolean meteredInput = ProgressMonitor.getDefault().shouldMeterInput(url, "GET"); + ProgressSource pi = null; + + if (meteredInput) { + pi = new ProgressSource(url, "GET", l); + pi.beginTracking(); } + + is = new MeteredStream(is, pi, l); } } catch (Exception e) { e.printStackTrace(); - /* do nothing, since all we were doing was trying to - get the size in bytes of the file */ + /* do nothing, since all we were doing was trying to + get the size in bytes of the file */ } - String type = guessContentTypeFromName(fullpath); - if (type == null && is.markSupported()) { - type = guessContentTypeFromStream(is); - } - if (type != null) { - msgh.add("content-type", type); + if (isAdir) { + msgh.add("content-type", "text/plain"); + msgh.add("access-type", "directory"); + } else { + msgh.add("access-type", "file"); + String ftype = guessContentTypeFromName(fullpath); + if (ftype == null && is.markSupported()) { + ftype = guessContentTypeFromStream(is); + } + if (ftype != null) { + msgh.add("content-type", ftype); + } } } catch (FileNotFoundException e) { try { cd(fullpath); /* if that worked, then make a directory listing - and build an html stream with all the files in - the directory */ - ftp.ascii(); + and build an html stream with all the files in + the directory */ + ftp.setAsciiType(); - is = new FtpInputStream(ftp, ftp.list()); + is = new FtpInputStream(ftp, ftp.list(null)); msgh.add("content-type", "text/plain"); + msgh.add("access-type", "directory"); } catch (IOException ex) { throw new FileNotFoundException(fullpath); + } catch (FtpProtocolException ex2) { + throw new FileNotFoundException(fullpath); } + } catch (FtpProtocolException ftpe) { + throw new IOException(ftpe); } setProperties(msgh); return is; @@ -477,31 +500,45 @@ public class FtpURLConnection extends URLConnection { * points to a directory * @throws FtpProtocolException if errors occur during the transfert. */ + @Override public OutputStream getOutputStream() throws IOException { if (!connected) { connect(); } - if (http != null) - return http.getOutputStream(); + if (http != null) { + OutputStream out = http.getOutputStream(); + // getInputStream() is neccessary to force a writeRequests() + // on the http client. + http.getInputStream(); + return out; + } - if (is != null) + if (is != null) { throw new IOException("Already opened for input"); + } if (os != null) { return os; } decodePath(url.getPath()); - if (filename == null || filename.length() == 0) + if (filename == null || filename.length() == 0) { throw new IOException("illegal filename for a PUT"); - if (pathname != null) - cd(pathname); - if (type == ASCII) - ftp.ascii(); - else - ftp.binary(); - os = new FtpOutputStream(ftp, ftp.put(filename)); + } + try { + if (pathname != null) { + cd(pathname); + } + if (type == ASCII) { + ftp.setAsciiType(); + } else { + ftp.setBinaryType(); + } + os = new FtpOutputStream(ftp, ftp.putFileStream(filename, false)); + } catch (FtpProtocolException e) { + throw new IOException(e); + } return os; } @@ -514,12 +551,13 @@ public class FtpURLConnection extends URLConnection { * * @return The Permission object. */ + @Override public Permission getPermission() { if (permission == null) { - int port = url.getPort(); - port = port < 0 ? FtpClient.FTP_PORT : port; - String host = this.host + ":" + port; - permission = new SocketPermission(host, "connect"); + int urlport = url.getPort(); + urlport = urlport < 0 ? FtpClient.defaultPort() : urlport; + String urlhost = this.host + ":" + urlport; + permission = new SocketPermission(urlhost, "connect"); } return permission; } @@ -534,21 +572,22 @@ public class FtpURLConnection extends URLConnection { * @throws IllegalStateException if already connected * @see #getRequestProperty(java.lang.String) */ + @Override public void setRequestProperty(String key, String value) { - super.setRequestProperty (key, value); - if ("type".equals (key)) { - if ("i".equalsIgnoreCase(value)) + super.setRequestProperty(key, value); + if ("type".equals(key)) { + if ("i".equalsIgnoreCase(value)) { type = BIN; - else if ("a".equalsIgnoreCase(value)) + } else if ("a".equalsIgnoreCase(value)) { type = ASCII; - else - if ("d".equalsIgnoreCase(value)) - type = DIR; - else + } else if ("d".equalsIgnoreCase(value)) { + type = DIR; + } else { throw new IllegalArgumentException( - "Value of '" + key + - "' request property was '" + value + - "' when it must be either 'i', 'a' or 'd'"); + "Value of '" + key + + "' request property was '" + value + + "' when it must be either 'i', 'a' or 'd'"); + } } } @@ -562,33 +601,41 @@ public class FtpURLConnection extends URLConnection { * @throws IllegalStateException if already connected * @see #setRequestProperty(java.lang.String, java.lang.String) */ + @Override public String getRequestProperty(String key) { - String value = super.getRequestProperty (key); + String value = super.getRequestProperty(key); if (value == null) { - if ("type".equals (key)) + if ("type".equals(key)) { value = (type == ASCII ? "a" : type == DIR ? "d" : "i"); + } } return value; } + @Override public void setConnectTimeout(int timeout) { - if (timeout < 0) + if (timeout < 0) { throw new IllegalArgumentException("timeouts can't be negative"); + } connectTimeout = timeout; } + @Override public int getConnectTimeout() { return (connectTimeout < 0 ? 0 : connectTimeout); } + @Override public void setReadTimeout(int timeout) { - if (timeout < 0) + if (timeout < 0) { throw new IllegalArgumentException("timeouts can't be negative"); + } readTimeout = timeout; } + @Override public int getReadTimeout() { return readTimeout < 0 ? 0 : readTimeout; } -- GitLab