1 /* FTPConnection.java --
2    Copyright (C) 2003, 2004  Free Software Foundation, Inc.
3 
4 This file is part of GNU Classpath.
5 
6 GNU Classpath is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2, or (at your option)
9 any later version.
10 
11 GNU Classpath is distributed in the hope that it will be useful, but
12 WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 General Public License for more details.
15 
16 You should have received a copy of the GNU General Public License
17 along with GNU Classpath; see the file COPYING.  If not, write to the
18 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 02110-1301 USA.
20 
21 Linking this library statically or dynamically with other modules is
22 making a combined work based on this library.  Thus, the terms and
23 conditions of the GNU General Public License cover the whole
24 combination.
25 
26 As a special exception, the copyright holders of this library give you
27 permission to link this library with independent modules to produce an
28 executable, regardless of the license terms of these independent
29 modules, and to copy and distribute the resulting executable under
30 terms of your choice, provided that you also meet, for each linked
31 independent module, the terms and conditions of the license of that
32 module.  An independent module is a module which is not derived from
33 or based on this library.  If you modify this library, you may extend
34 this exception to your version of the library, but you are not
35 obligated to do so.  If you do not wish to do so, delete this
36 exception statement from your version. */
37 
38 
39 package gnu.java.net.protocol.ftp;
40 
41 import gnu.java.net.CRLFInputStream;
42 import gnu.java.net.CRLFOutputStream;
43 import gnu.java.net.EmptyX509TrustManager;
44 import gnu.java.net.LineInputStream;
45 
46 import java.io.BufferedInputStream;
47 import java.io.BufferedOutputStream;
48 import java.io.IOException;
49 import java.io.InputStream;
50 import java.io.OutputStream;
51 import java.net.BindException;
52 import java.net.InetAddress;
53 import java.net.InetSocketAddress;
54 import java.net.ProtocolException;
55 import java.net.Socket;
56 import java.net.UnknownHostException;
57 import java.security.GeneralSecurityException;
58 import java.util.ArrayList;
59 import java.util.List;
60 
61 import javax.net.ssl.SSLContext;
62 import javax.net.ssl.SSLSocket;
63 import javax.net.ssl.SSLSocketFactory;
64 import javax.net.ssl.TrustManager;
65 
66 /**
67  * An FTP client connection, or PI.
68  * This implements RFC 959, with the following exceptions:
69  * <ul>
70  * <li>STAT, HELP, SITE, SMNT, and ACCT commands are not supported.</li>
71  * <li>the TYPE command does not allow alternatives to the default bytesize
72  * (Non-print), and local bytesize is not supported.</li>
73  * </ul>
74  *
75  * @author Chris Burdess (dog@gnu.org)
76  */
77 public class FTPConnection
78 {
79 
80   /**
81    * The default FTP transmission control port.
82    */
83   public static final int FTP_PORT = 21;
84 
85   /**
86    * The FTP data port.
87    */
88   public static final int FTP_DATA_PORT = 20;
89 
90   // -- FTP vocabulary --
91   protected static final String USER = "USER";
92   protected static final String PASS = "PASS";
93   protected static final String ACCT = "ACCT";
94   protected static final String CWD = "CWD";
95   protected static final String CDUP = "CDUP";
96   protected static final String SMNT = "SMNT";
97   protected static final String REIN = "REIN";
98   protected static final String QUIT = "QUIT";
99 
100   protected static final String PORT = "PORT";
101   protected static final String PASV = "PASV";
102   protected static final String TYPE = "TYPE";
103   protected static final String STRU = "STRU";
104   protected static final String MODE = "MODE";
105 
106   protected static final String RETR = "RETR";
107   protected static final String STOR = "STOR";
108   protected static final String STOU = "STOU";
109   protected static final String APPE = "APPE";
110   protected static final String ALLO = "ALLO";
111   protected static final String REST = "REST";
112   protected static final String RNFR = "RNFR";
113   protected static final String RNTO = "RNTO";
114   protected static final String ABOR = "ABOR";
115   protected static final String DELE = "DELE";
116   protected static final String RMD = "RMD";
117   protected static final String MKD = "MKD";
118   protected static final String PWD = "PWD";
119   protected static final String LIST = "LIST";
120   protected static final String NLST = "NLST";
121   protected static final String SITE = "SITE";
122   protected static final String SYST = "SYST";
123   protected static final String STAT = "STAT";
124   protected static final String HELP = "HELP";
125   protected static final String NOOP = "NOOP";
126 
127   protected static final String AUTH = "AUTH";
128   protected static final String PBSZ = "PBSZ";
129   protected static final String PROT = "PROT";
130   protected static final String CCC = "CCC";
131   protected static final String TLS = "TLS";
132 
133   public static final int TYPE_ASCII = 1;
134   public static final int TYPE_EBCDIC = 2;
135   public static final int TYPE_BINARY = 3;
136 
137   public static final int STRUCTURE_FILE = 1;
138   public static final int STRUCTURE_RECORD = 2;
139   public static final int STRUCTURE_PAGE = 3;
140 
141   public static final int MODE_STREAM = 1;
142   public static final int MODE_BLOCK = 2;
143   public static final int MODE_COMPRESSED = 3;
144 
145   // -- Telnet constants --
146   private static final String US_ASCII = "US-ASCII";
147 
148   /**
149    * The socket used to communicate with the server.
150    */
151   protected Socket socket;
152 
153   /**
154    * The socket input stream.
155    */
156   protected LineInputStream in;
157 
158   /**
159    * The socket output stream.
160    */
161   protected CRLFOutputStream out;
162 
163   /**
164    * The timeout when attempting to connect a socket.
165    */
166   protected int connectionTimeout;
167 
168   /**
169    * The read timeout on sockets.
170    */
171   protected int timeout;
172 
173   /**
174    * If true, print debugging information.
175    */
176   protected boolean debug;
177 
178   /**
179    * The current data transfer process in use by this connection.
180    */
181   protected DTP dtp;
182 
183   /**
184    * The current representation type.
185    */
186   protected int representationType = TYPE_ASCII;
187 
188   /**
189    * The current file structure type.
190    */
191   protected int fileStructure = STRUCTURE_FILE;
192 
193   /**
194    * The current transfer mode.
195    */
196   protected int transferMode = MODE_STREAM;
197 
198   /**
199    * If true, use passive mode.
200    */
201   protected boolean passive = false;
202 
203   /**
204    * Creates a new connection to the server using the default port.
205    * @param hostname the hostname of the server to connect to
206    */
FTPConnection(String hostname)207   public FTPConnection(String hostname)
208     throws UnknownHostException, IOException
209   {
210     this(hostname, -1, 0, 0, false);
211   }
212 
213   /**
214    * Creates a new connection to the server.
215    * @param hostname the hostname of the server to connect to
216    * @param port the port to connect to(if &lt;=0, use default port)
217    */
FTPConnection(String hostname, int port)218   public FTPConnection(String hostname, int port)
219     throws UnknownHostException, IOException
220   {
221     this(hostname, port, 0, 0, false);
222   }
223 
224   /**
225    * Creates a new connection to the server.
226    * @param hostname the hostname of the server to connect to
227    * @param port the port to connect to(if &lt;=0, use default port)
228    * @param connectionTimeout the connection timeout, in milliseconds
229    * @param timeout the I/O timeout, in milliseconds
230    * @param debug print debugging information
231    */
FTPConnection(String hostname, int port, int connectionTimeout, int timeout, boolean debug)232   public FTPConnection(String hostname, int port,
233                         int connectionTimeout, int timeout, boolean debug)
234     throws UnknownHostException, IOException
235   {
236     this.connectionTimeout = connectionTimeout;
237     this.timeout = timeout;
238     this.debug = debug;
239     if (port <= 0)
240       {
241         port = FTP_PORT;
242       }
243 
244     // Set up socket
245     socket = new Socket();
246     InetSocketAddress address = new InetSocketAddress(hostname, port);
247     if (connectionTimeout > 0)
248       {
249         socket.connect(address, connectionTimeout);
250       }
251     else
252       {
253         socket.connect(address);
254       }
255     if (timeout > 0)
256       {
257         socket.setSoTimeout(timeout);
258       }
259 
260     InputStream in = socket.getInputStream();
261     in = new BufferedInputStream(in);
262     in = new CRLFInputStream(in);
263     this.in = new LineInputStream(in);
264     OutputStream out = socket.getOutputStream();
265     out = new BufferedOutputStream(out);
266     this.out = new CRLFOutputStream(out);
267 
268     // Read greeting
269     FTPResponse response = getResponse();
270     switch (response.getCode())
271       {
272       case 220:                  // hello
273         break;
274       default:
275         throw new FTPException(response);
276       }
277   }
278 
279   /**
280    * Authenticate using the specified username and password.
281    * If the username suffices for the server, the password will not be used
282    * and may be null.
283    * @param username the username
284    * @param password the optional password
285    * @return true on success, false otherwise
286    */
authenticate(String username, String password)287   public boolean authenticate(String username, String password)
288     throws IOException
289   {
290     String cmd = USER + ' ' + username;
291     send(cmd);
292     FTPResponse response = getResponse();
293     switch (response.getCode())
294       {
295       case 230:                  // User logged in
296         return true;
297       case 331:                 // User name okay, need password
298         break;
299       case 332:                 // Need account for login
300       case 530:                 // No such user
301         return false;
302       default:
303         throw new FTPException(response);
304       }
305     cmd = PASS + ' ' + password;
306     send(cmd);
307     response = getResponse();
308     switch (response.getCode())
309       {
310       case 230:                  // User logged in
311       case 202:                  // Superfluous
312         return true;
313       case 332:                  // Need account for login
314       case 530:                  // Bad password
315         return false;
316       default:
317         throw new FTPException(response);
318       }
319   }
320 
321   /**
322    * Negotiates TLS over the current connection.
323    * See IETF draft-murray-auth-ftp-ssl-15.txt for details.
324    * @param confidential whether to provide confidentiality for the
325    * connection
326    */
starttls(boolean confidential)327   public boolean starttls(boolean confidential)
328     throws IOException
329   {
330     return starttls(confidential, new EmptyX509TrustManager());
331   }
332 
333   /**
334    * Negotiates TLS over the current connection.
335    * See IETF draft-murray-auth-ftp-ssl-15.txt for details.
336    * @param confidential whether to provide confidentiality for the
337    * connection
338    * @param tm the trust manager used to validate the server certificate.
339    */
starttls(boolean confidential, TrustManager tm)340   public boolean starttls(boolean confidential, TrustManager tm)
341     throws IOException
342   {
343     try
344       {
345         // Use SSLSocketFactory to negotiate a TLS session and wrap the
346         // current socket.
347         SSLContext context = SSLContext.getInstance("TLS");
348         // We don't require strong validation of the server certificate
349         TrustManager[] trust = new TrustManager[] { tm };
350         context.init(null, trust, null);
351         SSLSocketFactory factory = context.getSocketFactory();
352 
353         send(AUTH + ' ' + TLS);
354         FTPResponse response = getResponse();
355         switch (response.getCode())
356           {
357           case 500:
358           case 502:
359           case 504:
360           case 534:
361           case 431:
362             return false;
363           case 234:
364             break;
365           default:
366             throw new FTPException(response);
367           }
368 
369         String hostname = socket.getInetAddress().getHostName();
370         int port = socket.getPort();
371         SSLSocket ss =
372          (SSLSocket) factory.createSocket(socket, hostname, port, true);
373         String[] protocols = { "TLSv1", "SSLv3" };
374         ss.setEnabledProtocols(protocols);
375         ss.setUseClientMode(true);
376         ss.startHandshake();
377 
378         // PBSZ:PROT sequence
379         send(PBSZ + ' ' + Integer.MAX_VALUE);
380         response = getResponse();
381         switch (response.getCode())
382           {
383           case 501: // syntax error
384           case 503: // not authenticated
385             return false;
386           case 200:
387             break;
388           default:
389             throw new FTPException(response);
390           }
391         send(PROT + ' ' +(confidential ? 'P' : 'C'));
392         response = getResponse();
393         switch (response.getCode())
394           {
395           case 503: // not authenticated
396           case 504: // invalid level
397           case 536: // level not supported
398             return false;
399           case 200:
400             break;
401           default:
402             throw new FTPException(response);
403           }
404 
405         if (confidential)
406           {
407             // Set up streams
408             InputStream in = ss.getInputStream();
409             in = new BufferedInputStream(in);
410             in = new CRLFInputStream(in);
411             this.in = new LineInputStream(in);
412             OutputStream out = ss.getOutputStream();
413             out = new BufferedOutputStream(out);
414             this.out = new CRLFOutputStream(out);
415           }
416         return true;
417       }
418     catch (GeneralSecurityException e)
419       {
420         return false;
421       }
422   }
423 
424   /**
425    * Changes directory to the specified path.
426    * @param path an absolute or relative pathname
427    * @return true on success, false if the specified path does not exist
428    */
changeWorkingDirectory(String path)429   public boolean changeWorkingDirectory(String path)
430     throws IOException
431   {
432     String cmd = CWD + ' ' + path;
433     send(cmd);
434     FTPResponse response = getResponse();
435     switch (response.getCode())
436       {
437       case 250:
438         return true;
439       case 550:
440         return false;
441       default:
442         throw new FTPException(response);
443       }
444   }
445 
446   /**
447    * Changes directory to the parent of the current working directory.
448    * @return true on success, false otherwise
449    */
changeToParentDirectory()450   public boolean changeToParentDirectory()
451     throws IOException
452   {
453     send(CDUP);
454     FTPResponse response = getResponse();
455     switch (response.getCode())
456       {
457       case 250:
458         return true;
459       case 550:
460         return false;
461       default:
462         throw new FTPException(response);
463       }
464   }
465 
466   /**
467    * Terminates an authenticated login.
468    * If file transfer is in progress, it remains active for result response
469    * only.
470    */
reinitialize()471   public void reinitialize()
472     throws IOException
473   {
474     send(REIN);
475     FTPResponse response = getResponse();
476     switch (response.getCode())
477       {
478       case 220:
479         if (dtp != null)
480           {
481             dtp.complete();
482             dtp = null;
483           }
484         break;
485       default:
486         throw new FTPException(response);
487       }
488   }
489 
490   /**
491    * Terminates the control connection.
492    * The file transfer connection remains open for result response only.
493    * This connection is invalid and no further commands may be issued.
494    */
logout()495   public void logout()
496     throws IOException
497   {
498     send(QUIT);
499     try
500       {
501         getResponse();            // not required
502       }
503     catch (IOException e)
504       {
505       }
506     if (dtp != null)
507       {
508         dtp.complete();
509         dtp = null;
510       }
511     try
512       {
513         socket.close();
514       }
515     catch (IOException e)
516       {
517       }
518   }
519 
520   /**
521    * Initialise the data transfer process.
522    */
initialiseDTP()523   protected void initialiseDTP()
524     throws IOException
525   {
526     if (dtp != null)
527       {
528         dtp.complete();
529         dtp = null;
530       }
531 
532     InetAddress localhost = socket.getLocalAddress();
533     if (passive)
534       {
535         send(PASV);
536         FTPResponse response = getResponse();
537         switch (response.getCode())
538           {
539           case 227:
540             String message = response.getMessage();
541             try
542               {
543                 int start = message.indexOf(',');
544                 char c = message.charAt(start - 1);
545                 while (c >= 0x30 && c <= 0x39)
546                   {
547                     c = message.charAt((--start) - 1);
548                   }
549                 int mid1 = start;
550                 for (int i = 0; i < 4; i++)
551                   {
552                     mid1 = message.indexOf(',', mid1 + 1);
553                   }
554                 int mid2 = message.indexOf(',', mid1 + 1);
555                 if (mid1 == -1 || mid2 < mid1)
556                   {
557                     throw new ProtocolException("Malformed 227: " +
558                                                  message);
559                   }
560                 int end = mid2;
561                 c = message.charAt(end + 1);
562                 while (c >= 0x30 && c <= 0x39)
563                   {
564                     c = message.charAt((++end) + 1);
565                   }
566 
567                 String address =
568                   message.substring(start, mid1).replace(',', '.');
569                 int port_hi =
570                   Integer.parseInt(message.substring(mid1 + 1, mid2));
571                 int port_lo =
572                   Integer.parseInt(message.substring(mid2 + 1, end + 1));
573                 int port = (port_hi << 8) | port_lo;
574 
575                 /*System.out.println("Entering passive mode: " + address +
576                   ":" + port);*/
577                 dtp = new PassiveModeDTP(address, port, localhost,
578                                           connectionTimeout, timeout);
579                 break;
580               }
581             catch (ArrayIndexOutOfBoundsException e)
582               {
583                 throw new ProtocolException(e.getMessage() + ": " +
584                                              message);
585               }
586             catch (NumberFormatException e)
587               {
588                 throw new ProtocolException(e.getMessage() + ": " +
589                                              message);
590               }
591           default:
592             throw new FTPException(response);
593           }
594       }
595     else
596       {
597         // Get the local port
598         int port = socket.getLocalPort() + 1;
599         int tries = 0;
600         // Bind the active mode DTP
601         while (dtp == null)
602           {
603             try
604               {
605                 dtp = new ActiveModeDTP(localhost, port,
606                                          connectionTimeout, timeout);
607                 /*System.out.println("Listening on: " + port);*/
608               }
609             catch (BindException e)
610               {
611                 port++;
612                 tries++;
613                 if (tries > 9)
614                   {
615                     throw e;
616                   }
617               }
618           }
619 
620         // Send PORT command
621         StringBuffer buf = new StringBuffer(PORT);
622         buf.append(' ');
623         // Construct the address/port string form
624         byte[] address = localhost.getAddress();
625         for (int i = 0; i < address.length; i++)
626           {
627             int a =(int) address[i];
628             if (a < 0)
629               {
630                 a += 0x100;
631               }
632             buf.append(a);
633             buf.append(',');
634           }
635         int port_hi =(port & 0xff00) >> 8;
636         int port_lo =(port & 0x00ff);
637         buf.append(port_hi);
638         buf.append(',');
639         buf.append(port_lo);
640         send(buf.toString());
641         // Get response
642         FTPResponse response = getResponse();
643         switch (response.getCode())
644           {
645           case 200:                // OK
646             break;
647           default:
648             dtp.abort();
649             dtp = null;
650             throw new FTPException(response);
651           }
652       }
653     dtp.setTransferMode(transferMode);
654   }
655 
656   /**
657    * Set passive mode.
658    * @param flag true if we should use passive mode, false otherwise
659    */
setPassive(boolean flag)660   public void setPassive(boolean flag)
661     throws IOException
662   {
663     if (passive != flag)
664       {
665         passive = flag;
666         initialiseDTP();
667       }
668   }
669 
670   /**
671    * Returns the current representation type of the transfer data.
672    * @return TYPE_ASCII, TYPE_EBCDIC, or TYPE_BINARY
673    */
getRepresentationType()674   public int getRepresentationType()
675   {
676     return representationType;
677   }
678 
679   /**
680    * Sets the desired representation type of the transfer data.
681    * @param type TYPE_ASCII, TYPE_EBCDIC, or TYPE_BINARY
682    */
setRepresentationType(int type)683   public void setRepresentationType(int type)
684     throws IOException
685   {
686     StringBuffer buf = new StringBuffer(TYPE);
687     buf.append(' ');
688     switch (type)
689       {
690       case TYPE_ASCII:
691         buf.append('A');
692         break;
693       case TYPE_EBCDIC:
694         buf.append('E');
695         break;
696       case TYPE_BINARY:
697         buf.append('I');
698         break;
699       default:
700         throw new IllegalArgumentException(Integer.toString(type));
701       }
702     //buf.append(' ');
703     //buf.append('N');
704     send(buf.toString());
705     FTPResponse response = getResponse();
706     switch (response.getCode())
707       {
708       case 200:
709         representationType = type;
710         break;
711       default:
712         throw new FTPException(response);
713       }
714   }
715 
716   /**
717    * Returns the current file structure type.
718    * @return STRUCTURE_FILE, STRUCTURE_RECORD, or STRUCTURE_PAGE
719    */
getFileStructure()720   public int getFileStructure()
721   {
722     return fileStructure;
723   }
724 
725   /**
726    * Sets the desired file structure type.
727    * @param structure STRUCTURE_FILE, STRUCTURE_RECORD, or STRUCTURE_PAGE
728    */
setFileStructure(int structure)729   public void setFileStructure(int structure)
730     throws IOException
731   {
732     StringBuffer buf = new StringBuffer(STRU);
733     buf.append(' ');
734     switch (structure)
735       {
736       case STRUCTURE_FILE:
737         buf.append('F');
738         break;
739       case STRUCTURE_RECORD:
740         buf.append('R');
741         break;
742       case STRUCTURE_PAGE:
743         buf.append('P');
744         break;
745       default:
746         throw new IllegalArgumentException(Integer.toString(structure));
747       }
748     send(buf.toString());
749     FTPResponse response = getResponse();
750     switch (response.getCode())
751       {
752       case 200:
753         fileStructure = structure;
754         break;
755       default:
756         throw new FTPException(response);
757       }
758   }
759 
760   /**
761    * Returns the current transfer mode.
762    * @return MODE_STREAM, MODE_BLOCK, or MODE_COMPRESSED
763    */
getTransferMode()764   public int getTransferMode()
765   {
766     return transferMode;
767   }
768 
769   /**
770    * Sets the desired transfer mode.
771    * @param mode MODE_STREAM, MODE_BLOCK, or MODE_COMPRESSED
772    */
setTransferMode(int mode)773   public void setTransferMode(int mode)
774     throws IOException
775   {
776     StringBuffer buf = new StringBuffer(MODE);
777     buf.append(' ');
778     switch (mode)
779       {
780       case MODE_STREAM:
781         buf.append('S');
782         break;
783       case MODE_BLOCK:
784         buf.append('B');
785         break;
786       case MODE_COMPRESSED:
787         buf.append('C');
788         break;
789       default:
790         throw new IllegalArgumentException(Integer.toString(mode));
791       }
792     send(buf.toString());
793     FTPResponse response = getResponse();
794     switch (response.getCode())
795       {
796       case 200:
797         transferMode = mode;
798         if (dtp != null)
799           {
800             dtp.setTransferMode(mode);
801           }
802         break;
803       default:
804         throw new FTPException(response);
805       }
806   }
807 
808   /**
809    * Retrieves the specified file.
810    * @param filename the filename of the file to retrieve
811    * @return an InputStream containing the file content
812    */
retrieve(String filename)813   public InputStream retrieve(String filename)
814     throws IOException
815   {
816     if (dtp == null || transferMode == MODE_STREAM)
817       {
818         initialiseDTP();
819       }
820     /*
821        int size = -1;
822        String cmd = SIZE + ' ' + filename;
823        send(cmd);
824        FTPResponse response = getResponse();
825        switch (response.getCode())
826        {
827        case 213:
828        size = Integer.parseInt(response.getMessage());
829        break;
830        case 550: // File not found
831        default:
832        throw new FTPException(response);
833        }
834      */
835     String cmd = RETR + ' ' + filename;
836     send(cmd);
837     FTPResponse response = getResponse();
838     switch (response.getCode())
839       {
840       case 125:                  // Data connection already open; transfer starting
841       case 150:                  // File status okay; about to open data connection
842         return dtp.getInputStream();
843       default:
844         throw new FTPException(response);
845       }
846   }
847 
848   /**
849    * Returns a stream for uploading a file.
850    * If a file with the same filename already exists on the server, it will
851    * be overwritten.
852    * @param filename the name of the file to save the content as
853    * @return an OutputStream to write the file data to
854    */
store(String filename)855   public OutputStream store(String filename)
856     throws IOException
857   {
858     if (dtp == null || transferMode == MODE_STREAM)
859       {
860         initialiseDTP();
861       }
862     String cmd = STOR + ' ' + filename;
863     send(cmd);
864     FTPResponse response = getResponse();
865     switch (response.getCode())
866       {
867       case 125:                  // Data connection already open; transfer starting
868       case 150:                  // File status okay; about to open data connection
869         return dtp.getOutputStream();
870       default:
871         throw new FTPException(response);
872       }
873   }
874 
875   /**
876    * Returns a stream for uploading a file.
877    * If a file with the same filename already exists on the server, the
878    * content specified will be appended to the existing file.
879    * @param filename the name of the file to save the content as
880    * @return an OutputStream to write the file data to
881    */
append(String filename)882   public OutputStream append(String filename)
883     throws IOException
884   {
885     if (dtp == null || transferMode == MODE_STREAM)
886       {
887         initialiseDTP();
888       }
889     String cmd = APPE + ' ' + filename;
890     send(cmd);
891     FTPResponse response = getResponse();
892     switch (response.getCode())
893       {
894       case 125:                  // Data connection already open; transfer starting
895       case 150:                  // File status okay; about to open data connection
896         return dtp.getOutputStream();
897       default:
898         throw new FTPException(response);
899       }
900   }
901 
902   /**
903    * This command may be required by some servers to reserve sufficient
904    * storage to accommodate the new file to be transferred.
905    * It should be immediately followed by a <code>store</code> or
906    * <code>append</code>.
907    * @param size the number of bytes of storage to allocate
908    */
allocate(long size)909   public void allocate(long size)
910     throws IOException
911   {
912     String cmd = ALLO + ' ' + size;
913     send(cmd);
914     FTPResponse response = getResponse();
915     switch (response.getCode())
916       {
917       case 200:                  // OK
918       case 202:                  // Superfluous
919         break;
920       default:
921         throw new FTPException(response);
922       }
923   }
924 
925   /**
926    * Renames a file.
927    * @param oldName the current name of the file
928    * @param newName the new name
929    * @return true if successful, false otherwise
930    */
rename(String oldName, String newName)931   public boolean rename(String oldName, String newName)
932     throws IOException
933   {
934     String cmd = RNFR + ' ' + oldName;
935     send(cmd);
936     FTPResponse response = getResponse();
937     switch (response.getCode())
938       {
939       case 450:                  // File unavailable
940       case 550:                  // File not found
941         return false;
942       case 350:                 // Pending
943         break;
944       default:
945         throw new FTPException(response);
946       }
947     cmd = RNTO + ' ' + newName;
948     send(cmd);
949     response = getResponse();
950     switch (response.getCode())
951       {
952       case 250:                  // OK
953         return true;
954       case 450:
955       case 550:
956         return false;
957       default:
958         throw new FTPException(response);
959       }
960   }
961 
962   /**
963    * Aborts the transfer in progress.
964    * @return true if a transfer was in progress, false otherwise
965    */
abort()966   public boolean abort()
967     throws IOException
968   {
969     send(ABOR);
970     FTPResponse response = getResponse();
971     // Abort client DTP
972     if (dtp != null)
973       {
974         dtp.abort();
975       }
976     switch (response.getCode())
977       {
978       case 226:                  // successful abort
979         return false;
980       case 426:                 // interrupted
981         response = getResponse();
982         if (response.getCode() == 226)
983           {
984             return true;
985           }
986         // Otherwise fall through to throw exception
987       default:
988         throw new FTPException(response);
989       }
990   }
991 
992   /**
993    * Causes the file specified to be deleted at the server site.
994    * @param filename the file to delete
995    */
delete(String filename)996   public boolean delete(String filename)
997     throws IOException
998   {
999     String cmd = DELE + ' ' + filename;
1000     send(cmd);
1001     FTPResponse response = getResponse();
1002     switch (response.getCode())
1003       {
1004       case 250:                  // OK
1005         return true;
1006       case 450:                 // File unavailable
1007       case 550:                 // File not found
1008         return false;
1009       default:
1010         throw new FTPException(response);
1011       }
1012   }
1013 
1014   /**
1015    * Causes the directory specified to be deleted.
1016    * This may be an absolute or relative pathname.
1017    * @param pathname the directory to delete
1018    */
removeDirectory(String pathname)1019   public boolean removeDirectory(String pathname)
1020     throws IOException
1021   {
1022     String cmd = RMD + ' ' + pathname;
1023     send(cmd);
1024     FTPResponse response = getResponse();
1025     switch (response.getCode())
1026       {
1027       case 250:                  // OK
1028         return true;
1029       case 550:                 // File not found
1030         return false;
1031       default:
1032         throw new FTPException(response);
1033       }
1034   }
1035 
1036   /**
1037    * Causes the directory specified to be created at the server site.
1038    * This may be an absolute or relative pathname.
1039    * @param pathname the directory to create
1040    */
makeDirectory(String pathname)1041   public boolean makeDirectory(String pathname)
1042     throws IOException
1043   {
1044     String cmd = MKD + ' ' + pathname;
1045     send(cmd);
1046     FTPResponse response = getResponse();
1047     switch (response.getCode())
1048       {
1049       case 257:                  // Directory created
1050         return true;
1051       case 550:                 // File not found
1052         return false;
1053       default:
1054         throw new FTPException(response);
1055       }
1056   }
1057 
1058   /**
1059    * Returns the current working directory.
1060    */
getWorkingDirectory()1061   public String getWorkingDirectory()
1062     throws IOException
1063   {
1064     send(PWD);
1065     FTPResponse response = getResponse();
1066     switch (response.getCode())
1067       {
1068       case 257:
1069         String message = response.getMessage();
1070         if (message.charAt(0) == '"')
1071           {
1072             int end = message.indexOf('"', 1);
1073             if (end == -1)
1074               {
1075                 throw new ProtocolException(message);
1076               }
1077             return message.substring(1, end);
1078           }
1079         else
1080           {
1081             int end = message.indexOf(' ');
1082             if (end == -1)
1083               {
1084                 return message;
1085               }
1086             else
1087               {
1088                 return message.substring(0, end);
1089               }
1090           }
1091       default:
1092         throw new FTPException(response);
1093       }
1094   }
1095 
1096   /**
1097    * Returns a listing of information about the specified pathname.
1098    * If the pathname specifies a directory or other group of files, the
1099    * server should transfer a list of files in the specified directory.
1100    * If the pathname specifies a file then the server should send current
1101    * information on the file.  A null argument implies the user's
1102    * current working or default directory.
1103    * @param pathname the context pathname, or null
1104    */
list(String pathname)1105   public InputStream list(String pathname)
1106     throws IOException
1107   {
1108     if (dtp == null || transferMode == MODE_STREAM)
1109       {
1110         initialiseDTP();
1111       }
1112     if (pathname == null)
1113       {
1114         send(LIST);
1115       }
1116     else
1117       {
1118         String cmd = LIST + ' ' + pathname;
1119         send(cmd);
1120       }
1121     FTPResponse response = getResponse();
1122     switch (response.getCode())
1123       {
1124       case 125:                  // Data connection already open; transfer starting
1125       case 150:                  // File status okay; about to open data connection
1126         return dtp.getInputStream();
1127       default:
1128         throw new FTPException(response);
1129       }
1130   }
1131 
1132   /**
1133    * Returns a directory listing. The pathname should specify a
1134    * directory or other system-specific file group descriptor; a null
1135    * argument implies the user's current working or default directory.
1136    * @param pathname the directory pathname, or null
1137    * @return a list of filenames(strings)
1138    */
nameList(String pathname)1139   public List nameList(String pathname)
1140     throws IOException
1141   {
1142     if (dtp == null || transferMode == MODE_STREAM)
1143       {
1144         initialiseDTP();
1145       }
1146     if (pathname == null)
1147       {
1148         send(NLST);
1149       }
1150     else
1151       {
1152         String cmd = NLST + ' ' + pathname;
1153         send(cmd);
1154       }
1155     FTPResponse response = getResponse();
1156     switch (response.getCode())
1157       {
1158       case 125:                  // Data connection already open; transfer starting
1159       case 150:                  // File status okay; about to open data connection
1160         InputStream in = dtp.getInputStream();
1161         in = new BufferedInputStream(in);
1162         in = new CRLFInputStream(in);     // TODO ensure that TYPE is correct
1163         LineInputStream li = new LineInputStream(in);
1164         List ret = new ArrayList();
1165         for (String line = li.readLine();
1166              line != null;
1167              line = li.readLine())
1168           {
1169             ret.add(line);
1170           }
1171         li.close();
1172         return ret;
1173       default:
1174         throw new FTPException(response);
1175       }
1176   }
1177 
1178   /**
1179    * Returns the type of operating system at the server.
1180    */
system()1181   public String system()
1182     throws IOException
1183   {
1184     send(SYST);
1185     FTPResponse response = getResponse();
1186     switch (response.getCode())
1187       {
1188       case 215:
1189         String message = response.getMessage();
1190         int end = message.indexOf(' ');
1191         if (end == -1)
1192           {
1193             return message;
1194           }
1195         else
1196           {
1197             return message.substring(0, end);
1198           }
1199       default:
1200         throw new FTPException(response);
1201       }
1202   }
1203 
1204   /**
1205    * Does nothing.
1206    * This method can be used to ensure that the connection does not time
1207    * out.
1208    */
noop()1209   public void noop()
1210     throws IOException
1211   {
1212     send(NOOP);
1213     FTPResponse response = getResponse();
1214     switch (response.getCode())
1215       {
1216       case 200:
1217         break;
1218       default:
1219         throw new FTPException(response);
1220       }
1221   }
1222 
1223   // -- I/O --
1224 
1225   /**
1226    * Sends the specified command line to the server.
1227    * The CRLF sequence is automatically appended.
1228    * @param cmd the command line to send
1229    */
send(String cmd)1230   protected void send(String cmd)
1231     throws IOException
1232   {
1233     byte[] data = cmd.getBytes(US_ASCII);
1234     out.write(data);
1235     out.writeln();
1236     out.flush();
1237   }
1238 
1239   /**
1240    * Reads the next response from the server.
1241    * If the server sends the "transfer complete" code, this is handled here,
1242    * and the next response is passed to the caller.
1243    */
getResponse()1244   protected FTPResponse getResponse()
1245     throws IOException
1246   {
1247     FTPResponse response = readResponse();
1248     if (response.getCode() == 226)
1249       {
1250         if (dtp != null)
1251           {
1252             dtp.transferComplete();
1253           }
1254         response = readResponse();
1255       }
1256     return response;
1257   }
1258 
1259   /**
1260    * Reads and parses the next response from the server.
1261    */
readResponse()1262   protected FTPResponse readResponse()
1263     throws IOException
1264   {
1265     String line = in.readLine();
1266     if (line == null)
1267       {
1268         throw new ProtocolException( "EOF");
1269       }
1270     if (line.length() < 4)
1271       {
1272         throw new ProtocolException(line);
1273       }
1274     int code = parseCode(line);
1275     if (code == -1)
1276       {
1277         throw new ProtocolException(line);
1278       }
1279     char c = line.charAt(3);
1280     if (c == ' ')
1281       {
1282         return new FTPResponse(code, line.substring(4));
1283       }
1284     else if (c == '-')
1285       {
1286         StringBuffer buf = new StringBuffer(line.substring(4));
1287         buf.append('\n');
1288         while(true)
1289           {
1290             line = in.readLine();
1291             if (line == null)
1292               {
1293                 throw new ProtocolException("EOF");
1294               }
1295             if (line.length() >= 4 &&
1296                 line.charAt(3) == ' ' &&
1297                 parseCode(line) == code)
1298               {
1299                 return new FTPResponse(code, line.substring(4),
1300                                         buf.toString());
1301               }
1302             else
1303               {
1304                 buf.append(line);
1305                 buf.append('\n');
1306               }
1307           }
1308       }
1309     else
1310       {
1311         throw new ProtocolException(line);
1312       }
1313   }
1314 
1315   /*
1316    * Parses the 3-digit numeric code at the beginning of the given line.
1317    * Returns -1 on failure.
1318    */
parseCode(String line)1319   static final int parseCode(String line)
1320   {
1321     char[] c = { line.charAt(0), line.charAt(1), line.charAt(2) };
1322     int ret = 0;
1323     for (int i = 0; i < 3; i++)
1324       {
1325         int digit =((int) c[i]) - 0x30;
1326         if (digit < 0 || digit > 9)
1327           {
1328             return -1;
1329           }
1330         // Computing integer powers is way too expensive in Java!
1331         switch (i)
1332           {
1333           case 0:
1334             ret +=(100 * digit);
1335             break;
1336           case 1:
1337             ret +=(10 * digit);
1338             break;
1339           case 2:
1340             ret += digit;
1341             break;
1342           }
1343       }
1344     return ret;
1345   }
1346 
1347 }
1348 
1349