| /* FTPConnection.java -- |
| Copyright (C) 2003, 2004 Free Software Foundation, Inc. |
| |
| This file is part of GNU Classpath. |
| |
| GNU Classpath is free software; you can redistribute it and/or modify |
| it under the terms of the GNU General Public License as published by |
| the Free Software Foundation; either version 2, or (at your option) |
| any later version. |
| |
| GNU Classpath is distributed in the hope that it will be useful, but |
| WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| General Public License for more details. |
| |
| You should have received a copy of the GNU General Public License |
| along with GNU Classpath; see the file COPYING. If not, write to the |
| Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
| 02110-1301 USA. |
| |
| Linking this library statically or dynamically with other modules is |
| making a combined work based on this library. Thus, the terms and |
| conditions of the GNU General Public License cover the whole |
| combination. |
| |
| As a special exception, the copyright holders of this library give you |
| permission to link this library with independent modules to produce an |
| executable, regardless of the license terms of these independent |
| modules, and to copy and distribute the resulting executable under |
| terms of your choice, provided that you also meet, for each linked |
| independent module, the terms and conditions of the license of that |
| module. An independent module is a module which is not derived from |
| or based on this library. If you modify this library, you may extend |
| this exception to your version of the library, but you are not |
| obligated to do so. If you do not wish to do so, delete this |
| exception statement from your version. */ |
| |
| |
| package gnu.java.net.protocol.ftp; |
| |
| import gnu.java.net.CRLFInputStream; |
| import gnu.java.net.CRLFOutputStream; |
| import gnu.java.net.EmptyX509TrustManager; |
| import gnu.java.net.LineInputStream; |
| |
| import java.io.BufferedInputStream; |
| import java.io.BufferedOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.net.BindException; |
| import java.net.InetAddress; |
| import java.net.InetSocketAddress; |
| import java.net.ProtocolException; |
| import java.net.Socket; |
| import java.net.UnknownHostException; |
| import java.security.GeneralSecurityException; |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| import javax.net.ssl.SSLContext; |
| import javax.net.ssl.SSLSocket; |
| import javax.net.ssl.SSLSocketFactory; |
| import javax.net.ssl.TrustManager; |
| |
| /** |
| * An FTP client connection, or PI. |
| * This implements RFC 959, with the following exceptions: |
| * <ul> |
| * <li>STAT, HELP, SITE, SMNT, and ACCT commands are not supported.</li> |
| * <li>the TYPE command does not allow alternatives to the default bytesize |
| * (Non-print), and local bytesize is not supported.</li> |
| * </ul> |
| * |
| * @author Chris Burdess (dog@gnu.org) |
| */ |
| public class FTPConnection |
| { |
| |
| /** |
| * The default FTP transmission control port. |
| */ |
| public static final int FTP_PORT = 21; |
| |
| /** |
| * The FTP data port. |
| */ |
| public static final int FTP_DATA_PORT = 20; |
| |
| // -- FTP vocabulary -- |
| protected static final String USER = "USER"; |
| protected static final String PASS = "PASS"; |
| protected static final String ACCT = "ACCT"; |
| protected static final String CWD = "CWD"; |
| protected static final String CDUP = "CDUP"; |
| protected static final String SMNT = "SMNT"; |
| protected static final String REIN = "REIN"; |
| protected static final String QUIT = "QUIT"; |
| |
| protected static final String PORT = "PORT"; |
| protected static final String PASV = "PASV"; |
| protected static final String TYPE = "TYPE"; |
| protected static final String STRU = "STRU"; |
| protected static final String MODE = "MODE"; |
| |
| protected static final String RETR = "RETR"; |
| protected static final String STOR = "STOR"; |
| protected static final String STOU = "STOU"; |
| protected static final String APPE = "APPE"; |
| protected static final String ALLO = "ALLO"; |
| protected static final String REST = "REST"; |
| protected static final String RNFR = "RNFR"; |
| protected static final String RNTO = "RNTO"; |
| protected static final String ABOR = "ABOR"; |
| protected static final String DELE = "DELE"; |
| protected static final String RMD = "RMD"; |
| protected static final String MKD = "MKD"; |
| protected static final String PWD = "PWD"; |
| protected static final String LIST = "LIST"; |
| protected static final String NLST = "NLST"; |
| protected static final String SITE = "SITE"; |
| protected static final String SYST = "SYST"; |
| protected static final String STAT = "STAT"; |
| protected static final String HELP = "HELP"; |
| protected static final String NOOP = "NOOP"; |
| |
| protected static final String AUTH = "AUTH"; |
| protected static final String PBSZ = "PBSZ"; |
| protected static final String PROT = "PROT"; |
| protected static final String CCC = "CCC"; |
| protected static final String TLS = "TLS"; |
| |
| public static final int TYPE_ASCII = 1; |
| public static final int TYPE_EBCDIC = 2; |
| public static final int TYPE_BINARY = 3; |
| |
| public static final int STRUCTURE_FILE = 1; |
| public static final int STRUCTURE_RECORD = 2; |
| public static final int STRUCTURE_PAGE = 3; |
| |
| public static final int MODE_STREAM = 1; |
| public static final int MODE_BLOCK = 2; |
| public static final int MODE_COMPRESSED = 3; |
| |
| // -- Telnet constants -- |
| private static final String US_ASCII = "US-ASCII"; |
| |
| /** |
| * The socket used to communicate with the server. |
| */ |
| protected Socket socket; |
| |
| /** |
| * The socket input stream. |
| */ |
| protected LineInputStream in; |
| |
| /** |
| * The socket output stream. |
| */ |
| protected CRLFOutputStream out; |
| |
| /** |
| * The timeout when attempting to connect a socket. |
| */ |
| protected int connectionTimeout; |
| |
| /** |
| * The read timeout on sockets. |
| */ |
| protected int timeout; |
| |
| /** |
| * If true, print debugging information. |
| */ |
| protected boolean debug; |
| |
| /** |
| * The current data transfer process in use by this connection. |
| */ |
| protected DTP dtp; |
| |
| /** |
| * The current representation type. |
| */ |
| protected int representationType = TYPE_ASCII; |
| |
| /** |
| * The current file structure type. |
| */ |
| protected int fileStructure = STRUCTURE_FILE; |
| |
| /** |
| * The current transfer mode. |
| */ |
| protected int transferMode = MODE_STREAM; |
| |
| /** |
| * If true, use passive mode. |
| */ |
| protected boolean passive = false; |
| |
| /** |
| * Creates a new connection to the server using the default port. |
| * @param hostname the hostname of the server to connect to |
| */ |
| public FTPConnection(String hostname) |
| throws UnknownHostException, IOException |
| { |
| this(hostname, -1, 0, 0, false); |
| } |
| |
| /** |
| * Creates a new connection to the server. |
| * @param hostname the hostname of the server to connect to |
| * @param port the port to connect to(if <=0, use default port) |
| */ |
| public FTPConnection(String hostname, int port) |
| throws UnknownHostException, IOException |
| { |
| this(hostname, port, 0, 0, false); |
| } |
| |
| /** |
| * Creates a new connection to the server. |
| * @param hostname the hostname of the server to connect to |
| * @param port the port to connect to(if <=0, use default port) |
| * @param connectionTimeout the connection timeout, in milliseconds |
| * @param timeout the I/O timeout, in milliseconds |
| * @param debug print debugging information |
| */ |
| public FTPConnection(String hostname, int port, |
| int connectionTimeout, int timeout, boolean debug) |
| throws UnknownHostException, IOException |
| { |
| this.connectionTimeout = connectionTimeout; |
| this.timeout = timeout; |
| this.debug = debug; |
| if (port <= 0) |
| { |
| port = FTP_PORT; |
| } |
| |
| // Set up socket |
| socket = new Socket(); |
| InetSocketAddress address = new InetSocketAddress(hostname, port); |
| if (connectionTimeout > 0) |
| { |
| socket.connect(address, connectionTimeout); |
| } |
| else |
| { |
| socket.connect(address); |
| } |
| if (timeout > 0) |
| { |
| socket.setSoTimeout(timeout); |
| } |
| |
| InputStream in = socket.getInputStream(); |
| in = new BufferedInputStream(in); |
| in = new CRLFInputStream(in); |
| this.in = new LineInputStream(in); |
| OutputStream out = socket.getOutputStream(); |
| out = new BufferedOutputStream(out); |
| this.out = new CRLFOutputStream(out); |
| |
| // Read greeting |
| FTPResponse response = getResponse(); |
| switch (response.getCode()) |
| { |
| case 220: // hello |
| break; |
| default: |
| throw new FTPException(response); |
| } |
| } |
| |
| /** |
| * Authenticate using the specified username and password. |
| * If the username suffices for the server, the password will not be used |
| * and may be null. |
| * @param username the username |
| * @param password the optional password |
| * @return true on success, false otherwise |
| */ |
| public boolean authenticate(String username, String password) |
| throws IOException |
| { |
| String cmd = USER + ' ' + username; |
| send(cmd); |
| FTPResponse response = getResponse(); |
| switch (response.getCode()) |
| { |
| case 230: // User logged in |
| return true; |
| case 331: // User name okay, need password |
| break; |
| case 332: // Need account for login |
| case 530: // No such user |
| return false; |
| default: |
| throw new FTPException(response); |
| } |
| cmd = PASS + ' ' + password; |
| send(cmd); |
| response = getResponse(); |
| switch (response.getCode()) |
| { |
| case 230: // User logged in |
| case 202: // Superfluous |
| return true; |
| case 332: // Need account for login |
| case 530: // Bad password |
| return false; |
| default: |
| throw new FTPException(response); |
| } |
| } |
| |
| /** |
| * Negotiates TLS over the current connection. |
| * See IETF draft-murray-auth-ftp-ssl-15.txt for details. |
| * @param confidential whether to provide confidentiality for the |
| * connection |
| */ |
| public boolean starttls(boolean confidential) |
| throws IOException |
| { |
| return starttls(confidential, new EmptyX509TrustManager()); |
| } |
| |
| /** |
| * Negotiates TLS over the current connection. |
| * See IETF draft-murray-auth-ftp-ssl-15.txt for details. |
| * @param confidential whether to provide confidentiality for the |
| * connection |
| * @param tm the trust manager used to validate the server certificate. |
| */ |
| public boolean starttls(boolean confidential, TrustManager tm) |
| throws IOException |
| { |
| try |
| { |
| // Use SSLSocketFactory to negotiate a TLS session and wrap the |
| // current socket. |
| SSLContext context = SSLContext.getInstance("TLS"); |
| // We don't require strong validation of the server certificate |
| TrustManager[] trust = new TrustManager[] { tm }; |
| context.init(null, trust, null); |
| SSLSocketFactory factory = context.getSocketFactory(); |
| |
| send(AUTH + ' ' + TLS); |
| FTPResponse response = getResponse(); |
| switch (response.getCode()) |
| { |
| case 500: |
| case 502: |
| case 504: |
| case 534: |
| case 431: |
| return false; |
| case 234: |
| break; |
| default: |
| throw new FTPException(response); |
| } |
| |
| String hostname = socket.getInetAddress().getHostName(); |
| int port = socket.getPort(); |
| SSLSocket ss = |
| (SSLSocket) factory.createSocket(socket, hostname, port, true); |
| String[] protocols = { "TLSv1", "SSLv3" }; |
| ss.setEnabledProtocols(protocols); |
| ss.setUseClientMode(true); |
| ss.startHandshake(); |
| |
| // PBSZ:PROT sequence |
| send(PBSZ + ' ' + Integer.MAX_VALUE); |
| response = getResponse(); |
| switch (response.getCode()) |
| { |
| case 501: // syntax error |
| case 503: // not authenticated |
| return false; |
| case 200: |
| break; |
| default: |
| throw new FTPException(response); |
| } |
| send(PROT + ' ' +(confidential ? 'P' : 'C')); |
| response = getResponse(); |
| switch (response.getCode()) |
| { |
| case 503: // not authenticated |
| case 504: // invalid level |
| case 536: // level not supported |
| return false; |
| case 200: |
| break; |
| default: |
| throw new FTPException(response); |
| } |
| |
| if (confidential) |
| { |
| // Set up streams |
| InputStream in = ss.getInputStream(); |
| in = new BufferedInputStream(in); |
| in = new CRLFInputStream(in); |
| this.in = new LineInputStream(in); |
| OutputStream out = ss.getOutputStream(); |
| out = new BufferedOutputStream(out); |
| this.out = new CRLFOutputStream(out); |
| } |
| return true; |
| } |
| catch (GeneralSecurityException e) |
| { |
| return false; |
| } |
| } |
| |
| /** |
| * Changes directory to the specified path. |
| * @param path an absolute or relative pathname |
| * @return true on success, false if the specified path does not exist |
| */ |
| public boolean changeWorkingDirectory(String path) |
| throws IOException |
| { |
| // Do nothing if the path is empty. |
| if (path.length() == 0) |
| return true; |
| String cmd = CWD + ' ' + path; |
| send(cmd); |
| FTPResponse response = getResponse(); |
| switch (response.getCode()) |
| { |
| case 250: |
| return true; |
| case 550: |
| return false; |
| default: |
| throw new FTPException(response); |
| } |
| } |
| |
| /** |
| * Changes directory to the parent of the current working directory. |
| * @return true on success, false otherwise |
| */ |
| public boolean changeToParentDirectory() |
| throws IOException |
| { |
| send(CDUP); |
| FTPResponse response = getResponse(); |
| switch (response.getCode()) |
| { |
| case 250: |
| return true; |
| case 550: |
| return false; |
| default: |
| throw new FTPException(response); |
| } |
| } |
| |
| /** |
| * Terminates an authenticated login. |
| * If file transfer is in progress, it remains active for result response |
| * only. |
| */ |
| public void reinitialize() |
| throws IOException |
| { |
| send(REIN); |
| FTPResponse response = getResponse(); |
| switch (response.getCode()) |
| { |
| case 220: |
| if (dtp != null) |
| { |
| dtp.complete(); |
| dtp = null; |
| } |
| break; |
| default: |
| throw new FTPException(response); |
| } |
| } |
| |
| /** |
| * Terminates the control connection. |
| * The file transfer connection remains open for result response only. |
| * This connection is invalid and no further commands may be issued. |
| */ |
| public void logout() |
| throws IOException |
| { |
| send(QUIT); |
| try |
| { |
| getResponse(); // not required |
| } |
| catch (IOException e) |
| { |
| } |
| if (dtp != null) |
| { |
| dtp.complete(); |
| dtp = null; |
| } |
| try |
| { |
| socket.close(); |
| } |
| catch (IOException e) |
| { |
| } |
| } |
| |
| /** |
| * Initialise the data transfer process. |
| */ |
| protected void initialiseDTP() |
| throws IOException |
| { |
| if (dtp != null) |
| { |
| dtp.complete(); |
| dtp = null; |
| } |
| |
| InetAddress localhost = socket.getLocalAddress(); |
| if (passive) |
| { |
| send(PASV); |
| FTPResponse response = getResponse(); |
| switch (response.getCode()) |
| { |
| case 227: |
| String message = response.getMessage(); |
| try |
| { |
| int start = message.indexOf(','); |
| char c = message.charAt(start - 1); |
| while (c >= 0x30 && c <= 0x39) |
| { |
| c = message.charAt((--start) - 1); |
| } |
| int mid1 = start; |
| for (int i = 0; i < 4; i++) |
| { |
| mid1 = message.indexOf(',', mid1 + 1); |
| } |
| int mid2 = message.indexOf(',', mid1 + 1); |
| if (mid1 == -1 || mid2 < mid1) |
| { |
| throw new ProtocolException("Malformed 227: " + |
| message); |
| } |
| int end = mid2; |
| c = message.charAt(end + 1); |
| while (c >= 0x30 && c <= 0x39) |
| { |
| c = message.charAt((++end) + 1); |
| } |
| |
| String address = |
| message.substring(start, mid1).replace(',', '.'); |
| int port_hi = |
| Integer.parseInt(message.substring(mid1 + 1, mid2)); |
| int port_lo = |
| Integer.parseInt(message.substring(mid2 + 1, end + 1)); |
| int port = (port_hi << 8) | port_lo; |
| |
| /*System.out.println("Entering passive mode: " + address + |
| ":" + port);*/ |
| dtp = new PassiveModeDTP(address, port, localhost, |
| connectionTimeout, timeout); |
| break; |
| } |
| catch (ArrayIndexOutOfBoundsException e) |
| { |
| throw new ProtocolException(e.getMessage() + ": " + |
| message); |
| } |
| catch (NumberFormatException e) |
| { |
| throw new ProtocolException(e.getMessage() + ": " + |
| message); |
| } |
| default: |
| throw new FTPException(response); |
| } |
| } |
| else |
| { |
| // Get the local port |
| int port = socket.getLocalPort() + 1; |
| int tries = 0; |
| // Bind the active mode DTP |
| while (dtp == null) |
| { |
| try |
| { |
| dtp = new ActiveModeDTP(localhost, port, |
| connectionTimeout, timeout); |
| /*System.out.println("Listening on: " + port);*/ |
| } |
| catch (BindException e) |
| { |
| port++; |
| tries++; |
| if (tries > 9) |
| { |
| throw e; |
| } |
| } |
| } |
| |
| // Send PORT command |
| StringBuffer buf = new StringBuffer(PORT); |
| buf.append(' '); |
| // Construct the address/port string form |
| byte[] address = localhost.getAddress(); |
| for (int i = 0; i < address.length; i++) |
| { |
| int a =(int) address[i]; |
| if (a < 0) |
| { |
| a += 0x100; |
| } |
| buf.append(a); |
| buf.append(','); |
| } |
| int port_hi =(port & 0xff00) >> 8; |
| int port_lo =(port & 0x00ff); |
| buf.append(port_hi); |
| buf.append(','); |
| buf.append(port_lo); |
| send(buf.toString()); |
| // Get response |
| FTPResponse response = getResponse(); |
| switch (response.getCode()) |
| { |
| case 200: // OK |
| break; |
| default: |
| dtp.abort(); |
| dtp = null; |
| throw new FTPException(response); |
| } |
| } |
| dtp.setTransferMode(transferMode); |
| } |
| |
| /** |
| * Set passive mode. |
| * @param flag true if we should use passive mode, false otherwise |
| */ |
| public void setPassive(boolean flag) |
| throws IOException |
| { |
| if (passive != flag) |
| { |
| passive = flag; |
| initialiseDTP(); |
| } |
| } |
| |
| /** |
| * Returns the current representation type of the transfer data. |
| * @return TYPE_ASCII, TYPE_EBCDIC, or TYPE_BINARY |
| */ |
| public int getRepresentationType() |
| { |
| return representationType; |
| } |
| |
| /** |
| * Sets the desired representation type of the transfer data. |
| * @param type TYPE_ASCII, TYPE_EBCDIC, or TYPE_BINARY |
| */ |
| public void setRepresentationType(int type) |
| throws IOException |
| { |
| StringBuffer buf = new StringBuffer(TYPE); |
| buf.append(' '); |
| switch (type) |
| { |
| case TYPE_ASCII: |
| buf.append('A'); |
| break; |
| case TYPE_EBCDIC: |
| buf.append('E'); |
| break; |
| case TYPE_BINARY: |
| buf.append('I'); |
| break; |
| default: |
| throw new IllegalArgumentException(Integer.toString(type)); |
| } |
| //buf.append(' '); |
| //buf.append('N'); |
| send(buf.toString()); |
| FTPResponse response = getResponse(); |
| switch (response.getCode()) |
| { |
| case 200: |
| representationType = type; |
| break; |
| default: |
| throw new FTPException(response); |
| } |
| } |
| |
| /** |
| * Returns the current file structure type. |
| * @return STRUCTURE_FILE, STRUCTURE_RECORD, or STRUCTURE_PAGE |
| */ |
| public int getFileStructure() |
| { |
| return fileStructure; |
| } |
| |
| /** |
| * Sets the desired file structure type. |
| * @param structure STRUCTURE_FILE, STRUCTURE_RECORD, or STRUCTURE_PAGE |
| */ |
| public void setFileStructure(int structure) |
| throws IOException |
| { |
| StringBuffer buf = new StringBuffer(STRU); |
| buf.append(' '); |
| switch (structure) |
| { |
| case STRUCTURE_FILE: |
| buf.append('F'); |
| break; |
| case STRUCTURE_RECORD: |
| buf.append('R'); |
| break; |
| case STRUCTURE_PAGE: |
| buf.append('P'); |
| break; |
| default: |
| throw new IllegalArgumentException(Integer.toString(structure)); |
| } |
| send(buf.toString()); |
| FTPResponse response = getResponse(); |
| switch (response.getCode()) |
| { |
| case 200: |
| fileStructure = structure; |
| break; |
| default: |
| throw new FTPException(response); |
| } |
| } |
| |
| /** |
| * Returns the current transfer mode. |
| * @return MODE_STREAM, MODE_BLOCK, or MODE_COMPRESSED |
| */ |
| public int getTransferMode() |
| { |
| return transferMode; |
| } |
| |
| /** |
| * Sets the desired transfer mode. |
| * @param mode MODE_STREAM, MODE_BLOCK, or MODE_COMPRESSED |
| */ |
| public void setTransferMode(int mode) |
| throws IOException |
| { |
| StringBuffer buf = new StringBuffer(MODE); |
| buf.append(' '); |
| switch (mode) |
| { |
| case MODE_STREAM: |
| buf.append('S'); |
| break; |
| case MODE_BLOCK: |
| buf.append('B'); |
| break; |
| case MODE_COMPRESSED: |
| buf.append('C'); |
| break; |
| default: |
| throw new IllegalArgumentException(Integer.toString(mode)); |
| } |
| send(buf.toString()); |
| FTPResponse response = getResponse(); |
| switch (response.getCode()) |
| { |
| case 200: |
| transferMode = mode; |
| if (dtp != null) |
| { |
| dtp.setTransferMode(mode); |
| } |
| break; |
| default: |
| throw new FTPException(response); |
| } |
| } |
| |
| /** |
| * Retrieves the specified file. |
| * @param filename the filename of the file to retrieve |
| * @return an InputStream containing the file content |
| */ |
| public InputStream retrieve(String filename) |
| throws IOException |
| { |
| if (dtp == null || transferMode == MODE_STREAM) |
| { |
| initialiseDTP(); |
| } |
| /* |
| int size = -1; |
| String cmd = SIZE + ' ' + filename; |
| send(cmd); |
| FTPResponse response = getResponse(); |
| switch (response.getCode()) |
| { |
| case 213: |
| size = Integer.parseInt(response.getMessage()); |
| break; |
| case 550: // File not found |
| default: |
| throw new FTPException(response); |
| } |
| */ |
| String cmd = RETR + ' ' + filename; |
| send(cmd); |
| FTPResponse response = getResponse(); |
| switch (response.getCode()) |
| { |
| case 125: // Data connection already open; transfer starting |
| case 150: // File status okay; about to open data connection |
| return dtp.getInputStream(); |
| default: |
| throw new FTPException(response); |
| } |
| } |
| |
| /** |
| * Returns a stream for uploading a file. |
| * If a file with the same filename already exists on the server, it will |
| * be overwritten. |
| * @param filename the name of the file to save the content as |
| * @return an OutputStream to write the file data to |
| */ |
| public OutputStream store(String filename) |
| throws IOException |
| { |
| if (dtp == null || transferMode == MODE_STREAM) |
| { |
| initialiseDTP(); |
| } |
| String cmd = STOR + ' ' + filename; |
| send(cmd); |
| FTPResponse response = getResponse(); |
| switch (response.getCode()) |
| { |
| case 125: // Data connection already open; transfer starting |
| case 150: // File status okay; about to open data connection |
| return dtp.getOutputStream(); |
| default: |
| throw new FTPException(response); |
| } |
| } |
| |
| /** |
| * Returns a stream for uploading a file. |
| * If a file with the same filename already exists on the server, the |
| * content specified will be appended to the existing file. |
| * @param filename the name of the file to save the content as |
| * @return an OutputStream to write the file data to |
| */ |
| public OutputStream append(String filename) |
| throws IOException |
| { |
| if (dtp == null || transferMode == MODE_STREAM) |
| { |
| initialiseDTP(); |
| } |
| String cmd = APPE + ' ' + filename; |
| send(cmd); |
| FTPResponse response = getResponse(); |
| switch (response.getCode()) |
| { |
| case 125: // Data connection already open; transfer starting |
| case 150: // File status okay; about to open data connection |
| return dtp.getOutputStream(); |
| default: |
| throw new FTPException(response); |
| } |
| } |
| |
| /** |
| * This command may be required by some servers to reserve sufficient |
| * storage to accommodate the new file to be transferred. |
| * It should be immediately followed by a <code>store</code> or |
| * <code>append</code>. |
| * @param size the number of bytes of storage to allocate |
| */ |
| public void allocate(long size) |
| throws IOException |
| { |
| String cmd = ALLO + ' ' + size; |
| send(cmd); |
| FTPResponse response = getResponse(); |
| switch (response.getCode()) |
| { |
| case 200: // OK |
| case 202: // Superfluous |
| break; |
| default: |
| throw new FTPException(response); |
| } |
| } |
| |
| /** |
| * Renames a file. |
| * @param oldName the current name of the file |
| * @param newName the new name |
| * @return true if successful, false otherwise |
| */ |
| public boolean rename(String oldName, String newName) |
| throws IOException |
| { |
| String cmd = RNFR + ' ' + oldName; |
| send(cmd); |
| FTPResponse response = getResponse(); |
| switch (response.getCode()) |
| { |
| case 450: // File unavailable |
| case 550: // File not found |
| return false; |
| case 350: // Pending |
| break; |
| default: |
| throw new FTPException(response); |
| } |
| cmd = RNTO + ' ' + newName; |
| send(cmd); |
| response = getResponse(); |
| switch (response.getCode()) |
| { |
| case 250: // OK |
| return true; |
| case 450: |
| case 550: |
| return false; |
| default: |
| throw new FTPException(response); |
| } |
| } |
| |
| /** |
| * Aborts the transfer in progress. |
| * @return true if a transfer was in progress, false otherwise |
| */ |
| public boolean abort() |
| throws IOException |
| { |
| send(ABOR); |
| FTPResponse response = getResponse(); |
| // Abort client DTP |
| if (dtp != null) |
| { |
| dtp.abort(); |
| } |
| switch (response.getCode()) |
| { |
| case 226: // successful abort |
| return false; |
| case 426: // interrupted |
| response = getResponse(); |
| if (response.getCode() == 226) |
| { |
| return true; |
| } |
| // Otherwise fall through to throw exception |
| default: |
| throw new FTPException(response); |
| } |
| } |
| |
| /** |
| * Causes the file specified to be deleted at the server site. |
| * @param filename the file to delete |
| */ |
| public boolean delete(String filename) |
| throws IOException |
| { |
| String cmd = DELE + ' ' + filename; |
| send(cmd); |
| FTPResponse response = getResponse(); |
| switch (response.getCode()) |
| { |
| case 250: // OK |
| return true; |
| case 450: // File unavailable |
| case 550: // File not found |
| return false; |
| default: |
| throw new FTPException(response); |
| } |
| } |
| |
| /** |
| * Causes the directory specified to be deleted. |
| * This may be an absolute or relative pathname. |
| * @param pathname the directory to delete |
| */ |
| public boolean removeDirectory(String pathname) |
| throws IOException |
| { |
| String cmd = RMD + ' ' + pathname; |
| send(cmd); |
| FTPResponse response = getResponse(); |
| switch (response.getCode()) |
| { |
| case 250: // OK |
| return true; |
| case 550: // File not found |
| return false; |
| default: |
| throw new FTPException(response); |
| } |
| } |
| |
| /** |
| * Causes the directory specified to be created at the server site. |
| * This may be an absolute or relative pathname. |
| * @param pathname the directory to create |
| */ |
| public boolean makeDirectory(String pathname) |
| throws IOException |
| { |
| String cmd = MKD + ' ' + pathname; |
| send(cmd); |
| FTPResponse response = getResponse(); |
| switch (response.getCode()) |
| { |
| case 257: // Directory created |
| return true; |
| case 550: // File not found |
| return false; |
| default: |
| throw new FTPException(response); |
| } |
| } |
| |
| /** |
| * Returns the current working directory. |
| */ |
| public String getWorkingDirectory() |
| throws IOException |
| { |
| send(PWD); |
| FTPResponse response = getResponse(); |
| switch (response.getCode()) |
| { |
| case 257: |
| String message = response.getMessage(); |
| if (message.charAt(0) == '"') |
| { |
| int end = message.indexOf('"', 1); |
| if (end == -1) |
| { |
| throw new ProtocolException(message); |
| } |
| return message.substring(1, end); |
| } |
| else |
| { |
| int end = message.indexOf(' '); |
| if (end == -1) |
| { |
| return message; |
| } |
| else |
| { |
| return message.substring(0, end); |
| } |
| } |
| default: |
| throw new FTPException(response); |
| } |
| } |
| |
| /** |
| * Returns a listing of information about the specified pathname. |
| * If the pathname specifies a directory or other group of files, the |
| * server should transfer a list of files in the specified directory. |
| * If the pathname specifies a file then the server should send current |
| * information on the file. A null argument implies the user's |
| * current working or default directory. |
| * @param pathname the context pathname, or null |
| */ |
| public InputStream list(String pathname) |
| throws IOException |
| { |
| if (dtp == null || transferMode == MODE_STREAM) |
| { |
| initialiseDTP(); |
| } |
| if (pathname == null) |
| { |
| send(LIST); |
| } |
| else |
| { |
| String cmd = LIST + ' ' + pathname; |
| send(cmd); |
| } |
| FTPResponse response = getResponse(); |
| switch (response.getCode()) |
| { |
| case 125: // Data connection already open; transfer starting |
| case 150: // File status okay; about to open data connection |
| return dtp.getInputStream(); |
| default: |
| throw new FTPException(response); |
| } |
| } |
| |
| /** |
| * Returns a directory listing. The pathname should specify a |
| * directory or other system-specific file group descriptor; a null |
| * argument implies the user's current working or default directory. |
| * @param pathname the directory pathname, or null |
| * @return a list of filenames(strings) |
| */ |
| public List nameList(String pathname) |
| throws IOException |
| { |
| if (dtp == null || transferMode == MODE_STREAM) |
| { |
| initialiseDTP(); |
| } |
| if (pathname == null) |
| { |
| send(NLST); |
| } |
| else |
| { |
| String cmd = NLST + ' ' + pathname; |
| send(cmd); |
| } |
| FTPResponse response = getResponse(); |
| switch (response.getCode()) |
| { |
| case 125: // Data connection already open; transfer starting |
| case 150: // File status okay; about to open data connection |
| InputStream in = dtp.getInputStream(); |
| in = new BufferedInputStream(in); |
| in = new CRLFInputStream(in); // TODO ensure that TYPE is correct |
| LineInputStream li = new LineInputStream(in); |
| List ret = new ArrayList(); |
| for (String line = li.readLine(); |
| line != null; |
| line = li.readLine()) |
| { |
| ret.add(line); |
| } |
| li.close(); |
| return ret; |
| default: |
| throw new FTPException(response); |
| } |
| } |
| |
| /** |
| * Returns the type of operating system at the server. |
| */ |
| public String system() |
| throws IOException |
| { |
| send(SYST); |
| FTPResponse response = getResponse(); |
| switch (response.getCode()) |
| { |
| case 215: |
| String message = response.getMessage(); |
| int end = message.indexOf(' '); |
| if (end == -1) |
| { |
| return message; |
| } |
| else |
| { |
| return message.substring(0, end); |
| } |
| default: |
| throw new FTPException(response); |
| } |
| } |
| |
| /** |
| * Does nothing. |
| * This method can be used to ensure that the connection does not time |
| * out. |
| */ |
| public void noop() |
| throws IOException |
| { |
| send(NOOP); |
| FTPResponse response = getResponse(); |
| switch (response.getCode()) |
| { |
| case 200: |
| break; |
| default: |
| throw new FTPException(response); |
| } |
| } |
| |
| // -- I/O -- |
| |
| /** |
| * Sends the specified command line to the server. |
| * The CRLF sequence is automatically appended. |
| * @param cmd the command line to send |
| */ |
| protected void send(String cmd) |
| throws IOException |
| { |
| byte[] data = cmd.getBytes(US_ASCII); |
| out.write(data); |
| out.writeln(); |
| out.flush(); |
| } |
| |
| /** |
| * Reads the next response from the server. |
| * If the server sends the "transfer complete" code, this is handled here, |
| * and the next response is passed to the caller. |
| */ |
| protected FTPResponse getResponse() |
| throws IOException |
| { |
| FTPResponse response = readResponse(); |
| if (response.getCode() == 226) |
| { |
| if (dtp != null) |
| { |
| dtp.transferComplete(); |
| } |
| response = readResponse(); |
| } |
| return response; |
| } |
| |
| /** |
| * Reads and parses the next response from the server. |
| */ |
| protected FTPResponse readResponse() |
| throws IOException |
| { |
| String line = in.readLine(); |
| if (line == null) |
| { |
| throw new ProtocolException( "EOF"); |
| } |
| if (line.length() < 4) |
| { |
| throw new ProtocolException(line); |
| } |
| int code = parseCode(line); |
| if (code == -1) |
| { |
| throw new ProtocolException(line); |
| } |
| char c = line.charAt(3); |
| if (c == ' ') |
| { |
| return new FTPResponse(code, line.substring(4)); |
| } |
| else if (c == '-') |
| { |
| StringBuffer buf = new StringBuffer(line.substring(4)); |
| buf.append('\n'); |
| while(true) |
| { |
| line = in.readLine(); |
| if (line == null) |
| { |
| throw new ProtocolException("EOF"); |
| } |
| if (line.length() >= 4 && |
| line.charAt(3) == ' ' && |
| parseCode(line) == code) |
| { |
| return new FTPResponse(code, line.substring(4), |
| buf.toString()); |
| } |
| else |
| { |
| buf.append(line); |
| buf.append('\n'); |
| } |
| } |
| } |
| else |
| { |
| throw new ProtocolException(line); |
| } |
| } |
| |
| /* |
| * Parses the 3-digit numeric code at the beginning of the given line. |
| * Returns -1 on failure. |
| */ |
| static final int parseCode(String line) |
| { |
| char[] c = { line.charAt(0), line.charAt(1), line.charAt(2) }; |
| int ret = 0; |
| for (int i = 0; i < 3; i++) |
| { |
| int digit =((int) c[i]) - 0x30; |
| if (digit < 0 || digit > 9) |
| { |
| return -1; |
| } |
| // Computing integer powers is way too expensive in Java! |
| switch (i) |
| { |
| case 0: |
| ret +=(100 * digit); |
| break; |
| case 1: |
| ret +=(10 * digit); |
| break; |
| case 2: |
| ret += digit; |
| break; |
| } |
| } |
| return ret; |
| } |
| |
| } |
| |