View Javadoc

1   // FtpClientProtocol.java - a FTP client protocol implementation in Java
2   // $Id:FtpClientProtocol.java 77 2008-02-20 21:54:51Z sjardine $
3   //
4   // Copyright 1999, 2000 Joe Phillips <jaiger@innovationsw.com>
5   // Copyright 2001, 2002 Innovation Software Group, LLC - http://www.innovationsw.com
6   //
7   // for information on the HylaFAX FAX server see
8   //  http://www.hylafax.org/
9   //
10  // This library is free software; you can redistribute it and/or
11  // modify it under the terms of the GNU Library General Public
12  // License as published by the Free Software Foundation; either
13  // version 2 of the License, or (at your option) any later version.
14  //
15  // This library is distributed in the hope that it will be useful,
16  // but WITHOUT ANY WARRANTY; without even the implied warranty of
17  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
18  // Library General Public License for more details.
19  //
20  // You should have received a copy of the GNU Library General Public
21  // License along with this library; if not, write to the Free
22  // Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
23  //
24  // Todo:
25  //  - implement useful extensions (RFCs 2228, 2640, 2773 and STD0009)
26  //    - RFC2228 - security extensions
27  //    - RFC2640 - i18n
28  //    - RFC2773 - encryption with kea and skipjack
29  //
30  package gnu.inet.ftp;
31  
32  import java.io.BufferedReader;
33  import java.io.FileNotFoundException;
34  import java.io.IOException;
35  import java.io.InputStream;
36  import java.io.InputStreamReader;
37  import java.io.OutputStreamWriter;
38  import java.net.InetAddress;
39  import java.net.InetSocketAddress;
40  import java.net.Socket;
41  import java.net.SocketAddress;
42  import java.net.SocketException;
43  import java.net.UnknownHostException;
44  import java.text.ParseException;
45  import java.text.SimpleDateFormat;
46  import java.util.Calendar;
47  import java.util.Date;
48  import java.util.StringTokenizer;
49  import java.util.TimeZone;
50  import java.util.Vector;
51  
52  import org.apache.commons.logging.Log;
53  import org.apache.commons.logging.LogFactory;
54  
55  /***
56   * This is the core implementation of the FTP client protocol, RFC0959. You
57   * should be able to find the document via searches on the World Wide Web. At
58   * the time of this writing, RFCs could be found at <A
59   * HREF="http://www.faqs.org">www.faqs.org</A>.
60   * <P>
61   * The purpose of this class is to implement the FTP client protocol as simply
62   * and straight-forward as possible.
63   * <P>
64   * This package was started as an implementation of the HylaFAX client protocol
65   * so the features that overlap with the HylaFAX protocol will be the first
66   * implemented and most tested.
67   * <P>
68   * Method names are not my choosing for the most part. They have been largely
69   * pulled straight from the protocol and man pages. I expect that convenience
70   * classes and methods, with more developer friendly names will be built on top
71   * of this raw protocol implementation as time passes.
72   * <P>
73   * Most developers should use the higher-level FtpClient to perform some actions
74   * rather than this class directly.
75   * 
76   * @author <a href="mailto:jaiger@net-foundry.com">Joe Phillips</a>
77   * @author <a href="mailto:steve@mjnservices.com">Steven Jardine</a>
78   * @author <a href="mailto:dorpsidioot@gmail.com">Lieven Poelman</a>
79   * 
80   * @version $Id:FtpClientProtocol.java 77 2008-02-20 21:54:51Z sjardine $
81   */
82  public class FtpClientProtocol extends Object {
83  
84      private final static Log log = LogFactory.getLog(FtpClientProtocol.class);
85  
86      protected Socket sock; // socket for communications
87  
88      protected int port; // port to use
89  
90      protected BufferedReader istream; // buffered input stream
91  
92      protected OutputStreamWriter ostream; // output stream
93  
94      protected String greeting; // greeting string from server
95  
96      protected char fileType; // transfer file type
97  
98      protected int socketTimeout = 10000; // Socket timeout in Milliseconds. 0 =
99  
100     protected String hylafaxServerHost = null;
101 
102     protected int hylafaxServerPort = -1;
103 
104     protected String hylafaxServerUsername = null;
105 
106     // infinite.
107 
108     /***
109      * @param socketTimeout
110      *                timout in milliseconds.
111      */
112     public void setSocketTimeout(int socketTimeout) {
113 	this.socketTimeout = socketTimeout;
114     }
115 
116     /***
117      * @return the timout in milliseconds.
118      */
119     public int getSocketTimeout() {
120 	return socketTimeout;
121     }
122 
123     // public static stuff
124 
125     /***
126      * default FTP server port.
127      */
128     public static int DEFAULT_PORT = 21;
129 
130     /***
131      * default constructor. sets up the initial class state.
132      */
133     public FtpClientProtocol() {
134 	fileType = TYPE_ASCII; // default as per RFC 959
135     }
136 
137     // public methods
138 
139     /***
140      * get the local ip address of the control connection socket.
141      * 
142      * @return control connection ip address
143      */
144     public synchronized InetAddress getInetAddress() {
145 	return sock.getLocalAddress();
146     }
147 
148     /***
149      * open a connection to a given server and port this is an alias for
150      * connect()
151      * 
152      * @param host
153      *                the hostname of the HylaFAX server
154      * @param portnumber
155      *                the port the server is listening on
156      * @exception UnknownHostException
157      *                    cannot resolve the given hostname
158      * @exception IOException
159      *                    IO error occurred
160      * @exception ServerResponseException
161      *                    the server replied with an error code
162      */
163     public synchronized void open(String host, int portnumber)
164 	    throws UnknownHostException, IOException, ServerResponseException {
165 	connect(host, portnumber);
166     }
167 
168     /***
169      * open a connection to a given server at default port this is an alias for
170      * connect()
171      * 
172      * @param host
173      *                the hostname of the HylaFAX server
174      * @exception UnknownHostException
175      *                    cannot resolve the given hostname
176      * @exception IOException
177      *                    IO error occurred
178      * @exception ServerResponseException
179      *                    the server replied with an error code
180      */
181     public synchronized void open(String host) throws UnknownHostException,
182 	    IOException, ServerResponseException {
183 	connect(host, DEFAULT_PORT); // connect to default port
184     }
185 
186     /***
187      * open a connection to the localhost on the default port
188      * 
189      * @exception UnknownHostException
190      *                    cannot resolve the given hostname
191      * @exception IOException
192      *                    IO error occurred
193      * @exception ServerResponseException
194      *                    the server replied with an error code
195      */
196     public synchronized void open() throws UnknownHostException, IOException,
197 	    ServerResponseException {
198 	connect("localhost", DEFAULT_PORT);
199     }
200 
201     /***
202      * send the user name for this session
203      * 
204      * @param username
205      *                name of the user to login as
206      * @exception IOException
207      *                    io error occurred
208      * @exception ServerResponseException
209      *                    server replied with an error code
210      * @return true if a password is required, false if no password is required
211      */
212     public synchronized boolean user(String username) throws IOException,
213 	    ServerResponseException {
214 
215 	hylafaxServerUsername = username;
216 
217 	// send user command to server
218 	ostream.write("user " + username + "\r\n");
219 	ostream.flush();
220 	log.debug("-> user " + username);
221 
222 	// make sure command is accepted
223 	String response = readResponse(istream); // read a multi-line
224 	// response
225 	log.debug(response);
226 	String temp = new String(response.substring(0, 3));
227 	if (temp.equals("230")) {
228 	    return false;
229 	} else if (temp.equals("331")) {
230 	    return true; // password required, see pass()
231 	} else {
232 	    throw (new ServerResponseException(response));
233 	}
234     }
235 
236     /***
237      * send the password for this username and session
238      * 
239      * @param password
240      *                the password to login with
241      * @exception IOException
242      *                    io error occurred
243      * @exception ServerResponseException
244      *                    server replied with an error code
245      */
246     public synchronized void pass(String password) throws IOException,
247 	    ServerResponseException {
248 	// send pass command to server
249 	ostream.write("pass " + password + "\r\n");
250 	ostream.flush();
251 
252 	log.debug("-> pass");
253 
254 	// make sure command is accepted
255 	String response = readResponse(istream);
256 
257 	log.debug(response);
258 
259 	StringTokenizer st = new StringTokenizer(response, " -");
260 	if (!st.nextToken().equals("230")) {
261 	    throw (new ServerResponseException(response));
262 	}
263     }
264 
265     /***
266      * perform server No Operation could be used as a keep-alive
267      * 
268      * @exception IOException
269      *                    io error occurred
270      * @exception ServerResponseException
271      *                    server replied with an error code
272      */
273     public synchronized void noop() throws IOException, ServerResponseException {
274 	// send noop command to server
275 	ostream.write("noop\r\n");
276 	ostream.flush();
277 	log.debug("-> noop");
278 
279 	// make sure command is accepted
280 	String response = readResponse(istream);
281 	log.debug(response);
282 
283 	String temp = new String(response.substring(0, 3));
284 	if (!temp.equals("200")) {
285 	    throw (new ServerResponseException(response));
286 	}
287     }
288 
289     /***
290      * return current directory
291      * 
292      * @exception IOException
293      *                    io error occurred
294      * @exception ServerResponseException
295      *                    server replied with an error code
296      */
297     public synchronized String pwd() throws IOException,
298 	    ServerResponseException {
299 	// send pwd command to server
300 	ostream.write("pwd\r\n");
301 	ostream.flush();
302 	log.debug("-> pwd");
303 
304 	// check reply string
305 	String response = readResponse(istream);
306 	log.debug(response);
307 
308 	// Grab response code
309 	String temp = new String(response.substring(0, 3));
310 	if (!temp.equals("257")) {
311 	    // server didn't like command
312 	    throw (new ServerResponseException(response));
313 	}
314 	// get value of current directory
315 	StringTokenizer st = new StringTokenizer(response, "\"");
316 	st.nextToken();
317 	return st.nextToken();
318     }
319 
320     /***
321      * change current working directory
322      * 
323      * @param value
324      *                directory to set to current working directory
325      * @exception IOException
326      *                    io error occurred
327      * @exception ServerResponseException
328      *                    server replied with an error code
329      */
330     public synchronized void cwd(String value) throws IOException,
331 	    ServerResponseException {
332 	// send cwd command to server
333 	ostream.write("cwd " + value + "\r\n");
334 	ostream.flush();
335 	log.debug("-> cwd " + value);
336 
337 	// get reply
338 	String response = readResponse(istream);
339 	log.debug(response);
340 
341 	// get response code
342 	String temp = response.substring(0, 3);
343 	if (!"250".equals(temp)) {
344 	    // failed to change directories (probably doesn't exist)
345 	    throw (new ServerResponseException(response));
346 	}
347     }
348 
349     /***
350      * change to parent of current working directory
351      * 
352      * @exception IOException
353      *                    io error occurred
354      * @exception ServerResponseException
355      *                    server replied with error code
356      */
357     public synchronized void cdup() throws IOException, ServerResponseException {
358 	// send cdup command to the server
359 	ostream.write("cdup\r\n");
360 	ostream.flush();
361 	log.debug("-> cdup");
362 
363 	// get reply
364 	String response = readResponse(istream);
365 	log.debug(response);
366 
367 	// get response code
368 	StringTokenizer st = new StringTokenizer(response);
369 	if (!st.nextToken().equals("250")) {
370 	    throw (new ServerResponseException(response));
371 	}
372     }
373 
374     /***
375      * get the current idle timeout in seconds
376      * 
377      * @exception IOException
378      *                    io error occurred
379      * @exception ServerResponseException
380      *                    server replied with an error code
381      * @return server's idle timeout in seconds
382      */
383     public synchronized long idle() throws IOException, ServerResponseException {
384 	// send idle command to the server
385 	ostream.write("idle\r\n");
386 	ostream.flush();
387 	log.debug("-> idle");
388 
389 	// get response string
390 	String response = readResponse(istream);
391 	log.debug(response);
392 
393 	// check response code
394 	StringTokenizer st = new StringTokenizer(response);
395 	if (!st.nextToken().equals("213")) {
396 	    // command failed for some reason
397 	    throw (new ServerResponseException(response));
398 	}
399 	// get the data to return
400 	Long l = new Long(st.nextToken());
401 	return l.longValue();
402     }
403 
404     /***
405      * set the idle timeout value to the given number of seconds
406      * 
407      * @param timeout
408      *                new timeout value in seconds (MAX = 7200)
409      * @exception IOException
410      *                    io error occurred
411      * @exception ServerResponseException
412      *                    server replied with an error code
413      */
414     public synchronized void idle(long timeout) throws IOException,
415 	    ServerResponseException {
416 	// send idle command to the server
417 	ostream.write("idle " + timeout + "\r\n");
418 	ostream.flush();
419 	log.debug("-> idle " + timeout);
420 
421 	// get reply
422 	String response = readResponse(istream);
423 	log.debug(response);
424 
425 	// check result code
426 	StringTokenizer st = new StringTokenizer(response);
427 	if (!st.nextToken().equals("213")) {
428 	    // command failed
429 	    throw (new ServerResponseException(response));
430 	}
431     }
432 
433     /***
434      * delete the given file.
435      * 
436      * @param pathname
437      *                the name of the file to delete
438      * @exception IOException
439      *                    a socket IO error happened
440      * @exception ServerResponseException
441      *                    the server replied with an error code
442      */
443     public synchronized void dele(String pathname) throws IOException,
444 	    ServerResponseException {
445 	ostream.write("dele " + pathname + "\r\n");
446 	ostream.flush();
447 	log.debug("-> dele " + pathname);
448 
449 	String response = readResponse(istream);
450 	log.debug(response);
451 
452 	if (!response.substring(0, 3).equals("250")) {
453 	    throw (new ServerResponseException(response));
454 	}
455     }
456 
457     /***
458      * the file type is ASCII
459      */
460     public static final char TYPE_ASCII = 'A';
461 
462     /***
463      * the file type is EBCDIC
464      */
465     public static final char TYPE_EBCDIC = 'E';
466 
467     /***
468      * the file type is an 'image'. the file is binary data.
469      */
470     public static final char TYPE_IMAGE = 'I';
471 
472     /***
473      * the file type is a local type
474      */
475     public static final char TYPE_LOCAL = 'L';
476 
477     /***
478      * Set the file type for transfer. File types can be TYPE_ASCII,
479      * TYPE_EBCDIC, TYPE_IMAGE or TYPE_LOCAL. This may affect how the client and
480      * server interpret file data during transfers. The default type is
481      * TYPE_ASCII per RFC 959.
482      * 
483      * @param value
484      *                new type
485      * @exception IOException
486      *                    io error occurred
487      * @exception ServerResponseException
488      *                    server replied with error code
489      */
490     public synchronized void type(char value) throws IOException,
491 	    ServerResponseException {
492 	ostream.write("type " + value + "\r\n");
493 	ostream.flush();
494 	log.debug("-> type " + value);
495 
496 	String response = readResponse(istream);
497 	log.debug(response);
498 
499 	StringTokenizer st = new StringTokenizer(response);
500 	if (!st.nextToken().equals("200")) {
501 	    throw (new ServerResponseException(response));
502 	}
503 	fileType = value;
504     }
505 
506     /***
507      * Return the file type for transfer. File types can be TYPE_ASCII,
508      * TYPE_EBCDIC, TYPE_IMAGE, or TYPE_LOCAL.
509      * 
510      * @return current file transfer type
511      */
512     public char getType() {
513 	return fileType;
514     }
515 
516     // mode values
517 
518     /***
519      * data transfer mode is STREAM
520      */
521     public static final char MODE_STREAM = 'S';
522 
523     /***
524      * data transfer mode is BLOCK mode
525      */
526     public static final char MODE_BLOCK = 'B';
527 
528     /***
529      * data transfer mode is COMPRESSED, as in UNIX compress.
530      */
531     public static final char MODE_COMPRESSED = 'C';
532 
533     /***
534      * data transfer mode is ZLIB, as in zlib compression.
535      */
536     public static final char MODE_ZLIB = 'Z';
537 
538     /***
539      * set the data transfer mode. Valid modes are MODE_STREAM, MODE_BLOCK,
540      * MODE_COMPRESSED and MODE_ZLIB. NOTE: only MODE_STREAM (default) has been
541      * used to date.
542      * 
543      * @param value
544      *                new data transfer mode
545      * @exception IOException
546      *                    io error occurred
547      * @exception ServerResponseException
548      *                    server replied with an error code
549      */
550     public synchronized void mode(char value) throws IOException,
551 	    ServerResponseException {
552 	ostream.write("mode " + value + "\r\n");
553 	ostream.flush();
554 	log.debug("-> mode " + value);
555 
556 	String response = readResponse(istream);
557 	log.debug(response);
558 
559 	StringTokenizer st = new StringTokenizer(response);
560 	if (!st.nextToken().equals("200")) {
561 	    throw (new ServerResponseException(response));
562 	}
563     }
564 
565     /***
566      * abort the last command
567      * 
568      * @exception IOException
569      *                    a socket IO error occurred
570      * @exception ServerResponseException
571      *                    the server responded with an error code
572      */
573     public synchronized void abor() throws IOException, ServerResponseException {
574 	ostream.write("abor\r\n");
575 	ostream.flush();
576 	log.debug("-> abor");
577 
578 	String response = readResponse(istream);
579 	log.debug(response);
580 
581 	StringTokenizer st = new StringTokenizer(response);
582 	if (!st.nextToken().equals("225")) {
583 	    throw (new ServerResponseException(response));
584 	}
585     }
586 
587     /***
588      * abort the last command on the given modem. experience shows that this
589      * requires ADMIN priviledges.
590      * 
591      * @param modem
592      *                the modem to abort the command on
593      * @exception IOException
594      *                    a socket IO error occurred
595      * @exception ServerResponseException
596      *                    the server responded with an error
597      */
598     public synchronized void abor(String modem) throws IOException,
599 	    ServerResponseException {
600 	ostream.write("abor " + modem + "\r\n");
601 	ostream.flush();
602 	log.debug("-> abor " + modem);
603 
604 	String response = readResponse(istream);
605 	log.debug(response);
606 
607 	StringTokenizer st = new StringTokenizer(response);
608 	if (!st.nextToken().equals("225")) {
609 	    throw (new ServerResponseException(response));
610 	}
611     }
612 
613     /***
614      * tell the server which address/port we will listen on
615      * 
616      * @param address
617      *                address that we'll be listening on
618      * @param port
619      *                port on given address we'll be listening on
620      * @exception IOException
621      *                    io error occurred
622      * @exception ServerResponseException
623      *                    server replied with an error code
624      */
625     public synchronized void port(InetAddress address, int newPort)
626 	    throws IOException, ServerResponseException {
627 	// The PORT command sends a comma-delimited list of positive
628 	// decimal integers, each of which is one octet of the IP
629 	// address and port on which this client will accept a
630 	// data connection (for transfering data.) The bytes are
631 	// listed in network byte order (msb first.) An example
632 	// port command would be:
633 	// PORT a0,a1,a2,a3,p0,p1
634 	// where a0 and p0 are the msb's of the IP address and port
635 	// respectively.
636 
637 	// convert IP address into a form usable by PORT command
638 	String addr = address.getHostAddress();
639 
640 	// now turn all '.' characters into ',' characters
641 	addr = addr.replace('.', ',');
642 
643 	String str = new String("port " + addr + ","
644 		+ ((newPort & 0xff00) >> 8) + "," + (newPort & 0x00ff));
645 
646 	ostream.write(str + "\r\n");
647 	ostream.flush();
648 	log.debug("-> " + str + " (" + addr.replace(',', '.') + ":" + newPort
649 		+ ")");
650 
651 	String response = readResponse(istream);
652 	log.debug(response);
653 
654 	StringTokenizer st = new StringTokenizer(response);
655 	if (!st.nextToken().equals("200")) {
656 	    throw (new ServerResponseException(response));
657 	}
658     }
659 
660     /***
661      * store temp file, the file is stored in a uniquely named file on the
662      * server. The remote temp file is deleted when the connection is closed.
663      * 
664      * @exception IOException
665      *                    io error occurred talking to the server
666      * @exception ServerResponseException
667      *                    server replied with error code
668      * @return the filename of the temp file
669      */
670     public synchronized String stot(InputStream data) throws IOException,
671 	    ServerResponseException {
672 	String filename;
673 	String response;
674 	StringTokenizer st;
675 
676 	// send stot command to server
677 	ostream.write("stot\r\n");
678 	ostream.flush();
679 
680 	log.debug("-> stot");
681 
682 	// get results
683 	response = readResponse(istream);
684 	log.debug(response);
685 
686 	st = new StringTokenizer(response);
687 	if (!st.nextToken().equals("150")) {
688 	    throw (new ServerResponseException(response));
689 	}
690 	st.nextToken(); // ignore 'FILE:' string
691 	filename = new String(st.nextToken()); // get filename value
692 
693 	// transfering ...
694 
695 	// next line tells us transfer completed
696 	response = readResponse(istream);
697 	log.debug(response);
698 
699 	st = new StringTokenizer(response);
700 	if (!st.nextToken().equals("226")) {
701 	    // some sort of error
702 	    throw (new ServerResponseException(response));
703 	}
704 
705 	return filename;
706     }
707 
708     /***
709      * store a file with a unique name.
710      * 
711      * @exception IOException
712      *                    a socket IO error occurred
713      * @exception ServerResponseException
714      *                    the server responded with an error code
715      * @return the name of the file created
716      */
717     public synchronized String stou(InputStream in) throws IOException,
718 	    ServerResponseException {
719 	String filename;
720 	String response;
721 	StringTokenizer st;
722 
723 	// send stou command to server
724 	ostream.write("stou\r\n");
725 	ostream.flush();
726 
727 	log.debug("-> stou");
728 
729 	// get results
730 	response = readResponse(istream);
731 	log.debug(response);
732 
733 	st = new StringTokenizer(response);
734 	if (!st.nextToken().equals("150")) {
735 	    throw (new ServerResponseException(response));
736 	}
737 	st.nextToken(); // ignore 'FILE:' string
738 	filename = new String(st.nextToken()); // get filename value
739 
740 	// transfering ...
741 
742 	// next line tell us transfer completed
743 	response = readResponse(istream);
744 	log.debug(response);
745 
746 	st = new StringTokenizer(response);
747 	if (!st.nextToken().equals("226")) {
748 	    // some sort of error
749 	    throw (new ServerResponseException(response));
750 	}
751 
752 	return filename;
753     }
754 
755     /***
756      * store a file.
757      * 
758      * @param pathname
759      *                name of file to store on server (where to put the file on
760      *                the server)
761      * @exception IOException
762      *                    a socket IO error occurred
763      * @exception ServerResponseException
764      *                    the server responded with an error
765      */
766     public synchronized void stor(InputStream in, String pathname)
767 	    throws IOException, ServerResponseException {
768 
769 	// tell server to start transfer
770 	ostream.write("stor " + pathname + "\r\n");
771 	ostream.flush();
772 	log.debug("-> stor " + pathname);
773 
774 	String response = readResponse(istream);
775 	log.debug(response);
776 
777 	if (!response.substring(0, 3).equals("150")) {
778 	    throw (new ServerResponseException(response));
779 	}
780 
781 	// transfer is happening
782 
783 	// next line should indicate transfer is complete
784 	response = readResponse(istream);
785 	log.debug(response);
786 
787 	if (!response.substring(0, 3).equals("226")) {
788 	    throw (new ServerResponseException(response));
789 	}
790 
791 	// done
792     }
793 
794     /***
795      * get system type returns the string that the server sends (not sure how to
796      * handle it yet) on a Debian GNU/Linux 2.1 (slink) system, the result
797      * string is: "UNIX Type: L8 Version: SVR4"
798      * 
799      * @exception IOException
800      *                    a socket IO error occurred
801      * @exception ServerResponseException
802      *                    the server replied with an error code
803      * @return the system type string
804      */
805     public synchronized String syst() throws IOException,
806 	    ServerResponseException {
807 	ostream.write("syst\r\n");
808 	ostream.flush();
809 	log.debug("-> syst");
810 
811 	// get response
812 	String response = readResponse(istream);
813 	log.debug(response);
814 
815 	StringTokenizer st = new StringTokenizer(response);
816 	if (!st.nextToken().equals("215")) {
817 	    throw (new ServerResponseException(response));
818 	}
819 
820 	// get string to return
821 	return response.substring(4);
822     }
823 
824     // stru values
825 
826     /***
827      * the file structure is FILE.
828      */
829     public static final char STRU_FILE = 'F'; // file structure
830 
831     /***
832      * the file structure is RECORD based.
833      */
834     public static final char STRU_RECORD = 'R'; // record structure
835 
836     /***
837      * the file structure is PAGE based.
838      */
839     public static final char STRU_PAGE = 'P'; // page structure
840 
841     /***
842      * the file structure is TIFF.
843      */
844     public static final char STRU_TIFF = 'T'; // TIFF structure
845 
846     /***
847      * set the file structure. valid file structure settings are STRU_FILE,
848      * STRU_RECORD, STRU_PAGE and STRU_TIFF. NOTE: only STRU_FILE has been
849      * tested to date. I have no idea when you would use the other settings.
850      * 
851      * @param value
852      *                file structure setting
853      * @exception IOException
854      *                    io error occurred
855      * @exception ServerResponseException
856      *                    server replied with an error code
857      */
858     public synchronized void stru(char value) throws IOException,
859 	    ServerResponseException {
860 	ostream.write("stru " + value + "\r\n");
861 	ostream.flush();
862 	log.debug("-> stru " + value);
863 
864 	// check response
865 	String response = readResponse(istream);
866 	log.debug(response);
867 
868 	StringTokenizer st = new StringTokenizer(response);
869 	if (!st.nextToken().equals("200")) {
870 	    throw (new ServerResponseException(response));
871 	}
872     }
873 
874     /***
875      * get a list of files in a directory. Like nlst() but with more information
876      * per line. The difference is more like ls vs. ls -l. With FTP servers, I
877      * know the output is very system dependant but since there aren't too many
878      * different implementations of the HylaFAX server, it's probably not much
879      * of an issue here. Still, beware.
880      * <P>
881      * This method relies on a data port (passive or active) to receive the list
882      * data. it is recommended that you use the getList() wrapper method rather
883      * than this method directly.
884      * 
885      * @param path
886      *                path of file or directory to get listing. A <i>null</i>
887      *                path will get the listing of the current directory.
888      * @exception IOException
889      *                    io error occurred
890      * @exception ServerResponseException
891      *                    server replied with an error code
892      * @exception FileNotFoundException
893      *                    the given path does not exist
894      */
895     public synchronized void list(String path) throws IOException,
896 	    FileNotFoundException, ServerResponseException {
897 
898 	String command;
899 	if (path == null) {
900 	    command = "list";
901 	} else {
902 	    command = "list " + path;
903 	}
904 	ostream.write(command + "\r\n");
905 	ostream.flush();
906 	log.debug("-> " + command);
907 
908 	// get response
909 	String response = readResponse(istream);
910 	log.debug(response);
911 
912 	// check response
913 	StringTokenizer st = new StringTokenizer(response);
914 	String result_code = new String(st.nextToken());
915 	if (!result_code.equals("150")) {
916 	    if (result_code.equals("550")) {
917 		// "No such file or directory"
918 		throw (new FileNotFoundException(response));
919 	    }
920 	    throw (new ServerResponseException(response));
921 	}
922 
923 	// transferring ...
924 
925 	// next line of response
926 	response = readResponse(istream);
927 	log.debug(response);
928 
929 	st = new StringTokenizer(response);
930 	if (!st.nextToken().equals("226")) {
931 	    throw (new ServerResponseException(response));
932 	}
933 
934     }
935 
936     /***
937      * get list of files in current directory.
938      * <P>
939      * NOTE: uses list() with the <i>null</i> path.
940      * <P>
941      * This method relies on a data socket of some sort to receive the list
942      * data. It is recommended that you use the getList() convenience method
943      * rather than this one directly.
944      * 
945      * @exception IOException
946      *                    io error occurred
947      * @exception ServerResponseException
948      *                    server replied with an error code
949      * @exception FileNotFoundException
950      *                    server could not find the specified file
951      */
952     public synchronized void list() throws IOException, FileNotFoundException,
953 	    ServerResponseException {
954 	list(null);
955     }
956 
957     /***
958      * get name list of files in directory. Similar to list() but names only.
959      * <P>
960      * This method requires a data socket to receive the name list data. It is
961      * recommended that you use the getNameList() method instead of this method
962      * directly.
963      * 
964      * @param path
965      *                path of the file or directory to list, passing in <i>null</i>
966      *                will result in listing the current directory
967      * @exception IOException
968      *                    io error occurred
969      * @exception ServerResponseException
970      *                    server replied with an error code
971      * @exception FileNotFoundException
972      *                    server could not find the specified file
973      */
974     public synchronized void nlst(String path) throws IOException,
975 	    ServerResponseException, FileNotFoundException {
976 
977 	// initiate the nlst command...
978 	String command;
979 	if (path == null) {
980 	    command = "nlst";
981 	} else {
982 	    command = "nlst " + path;
983 	}
984 	ostream.write(command + "\r\n");
985 	ostream.flush();
986 	log.debug("-> " + command);
987 
988 	// check response
989 	String response = readResponse(istream);
990 	log.debug(response);
991 
992 	// check response
993 	String result_code = response.substring(0, 3);
994 	if (!"150".equals(result_code)) {
995 	    if ("550".equals(result_code)) {
996 		// file not found - "no such file or directory"
997 		throw (new FileNotFoundException(response));
998 	    }
999 	    throw (new ServerResponseException(response));
1000 	}
1001 
1002 	// transferring...
1003 
1004 	// check next line of response
1005 	response = readResponse(istream);
1006 	log.debug(response);
1007 
1008 	result_code = response.substring(0, 3);
1009 	if (!"226".equals(result_code)) {
1010 	    throw (new ServerResponseException(response));
1011 	}
1012 
1013 	// transfer complete
1014 
1015     }
1016 
1017     /***
1018      * get name list of files in directory. Similar to list() but names only.
1019      * NOTE: uses the <i>null</i> path as the argument to nlst(String)
1020      * 
1021      * @exception IOException
1022      *                    io error occurred
1023      * @exception ServerResponseException
1024      *                    server replied with an error code
1025      * @exception FileNotFoundException
1026      *                    server could not find the specified file
1027      */
1028     public synchronized void nlst() throws IOException, FileNotFoundException,
1029 	    ServerResponseException {
1030 	nlst(null);
1031     }
1032 
1033     /***
1034      * retrieve the given file
1035      * 
1036      * @param path
1037      *                the relative or absolute path to the file to retrieve
1038      * @exception IOException
1039      *                    caused by a socket IO error
1040      * @exception ServerResponseException
1041      *                    caused by a server response indicating an error
1042      * @exception FileNotFoundException
1043      *                    the given path does not exist
1044      */
1045     public synchronized void retr(String path) throws IOException,
1046 	    FileNotFoundException, ServerResponseException {
1047 	String response;
1048 	StringTokenizer st;
1049 
1050 	// send retr command to server
1051 	ostream.write("retr " + path + "\r\n");
1052 	ostream.flush();
1053 
1054 	log.debug("-> retr " + path);
1055 
1056 	// get results
1057 	response = readResponse(istream);
1058 	log.debug(response);
1059 
1060 	st = new StringTokenizer(response);
1061 	String code = st.nextToken();
1062 	if (!code.equals("150")) {
1063 	    if (code.equals("550")) {
1064 		// path not found
1065 		throw (new FileNotFoundException(response));
1066 	    }
1067 	    throw (new ServerResponseException(response));
1068 	}
1069 
1070 	// transfering ...
1071 
1072 	// next line tells us transfer completed
1073 	response = readResponse(istream);
1074 	log.debug(response);
1075 
1076 	st = new StringTokenizer(response);
1077 	if (!st.nextToken().equals("226")) {
1078 	    // some sort of error
1079 	    throw (new ServerResponseException(response));
1080 	}
1081     }
1082 
1083     /***
1084      * Returns the size (in bytes) of the given regular file. This is the size
1085      * on the server and may not accurately represent the file size once the
1086      * file has been transferred (particularly via ASCII mode)
1087      * 
1088      * @param pathname
1089      *                the name of the file to get the size for
1090      * @exception IOException
1091      *                    caused by a socket IO error
1092      * @exception ServerResponseException
1093      *                    caused by a server response indicating an error
1094      * @exception FileNotFoundException
1095      *                    the given path does not exist
1096      */
1097     public synchronized long size(String pathname) throws IOException,
1098 	    FileNotFoundException, ServerResponseException {
1099 	ostream.write("size " + pathname + "\r\n");
1100 	ostream.flush();
1101 	log.debug("-> size " + pathname);
1102 
1103 	String response = readResponse(istream);
1104 	log.debug(response);
1105 
1106 	StringTokenizer st = new StringTokenizer(response);
1107 	String return_code = st.nextToken();
1108 	if (!return_code.equals("213")) {
1109 	    if (return_code.equals("550")) {
1110 		throw (new FileNotFoundException(response));
1111 	    }
1112 	    throw (new ServerResponseException(response));
1113 	}
1114 	// get file size from response
1115 	return Long.parseLong(st.nextToken());
1116     }
1117 
1118     public final static String MDTM_TIME_FORMAT1 = "yyyyMMddHHmmss.SSS";
1119 
1120     public final static String MDTM_TIME_FORMAT2 = "yyyyMMddHHmmss";
1121 
1122     /***
1123      * Returns the last modified time of the given file. This command is
1124      * specified in the FTPEXT Working Group draft currently available at <code>
1125      * http://www.ietf.org/internet-drafts/draft-ietf-ftpext-mlst-15.txt</code>.
1126      * <p>
1127      * The date is returned in GMT in a string of the following format:
1128      * <code>YYYYMMDDhhmmss.ddd</code> where <code>.ddd</code> is an
1129      * optional suffix reporting milliseconds. This method attempts to parse the
1130      * string returned by the server and present the caller with a
1131      * java.util.Date instance.
1132      * 
1133      * @param pathname
1134      *                the name of the file to get the last-modified time for
1135      * @exception IOException
1136      *                    caused by a socket IO error
1137      * @exception ServerResponseException
1138      *                    caused by a server response indicating an error
1139      * @exception FileNotFoundException
1140      *                    the given path does not exist
1141      * @exception ParseException
1142      *                    the server returned an unrecognized date format
1143      */
1144     public synchronized Date mdtm(String pathname) throws IOException,
1145 	    FileNotFoundException, ServerResponseException, ParseException {
1146 	ostream.write("mdtm " + pathname + "\r\n");
1147 	ostream.flush();
1148 	log.debug("-> mdtm " + pathname);
1149 
1150 	String response = readResponse(istream);
1151 	log.debug(response);
1152 
1153 	StringTokenizer st = new StringTokenizer(response);
1154 	String return_code = st.nextToken();
1155 	if (!return_code.equals("213")) {
1156 	    if (return_code.equals("550")) {
1157 		throw new FileNotFoundException(response);
1158 	    }
1159 	    throw new ServerResponseException(response);
1160 	}
1161 
1162 	String time = st.nextToken();
1163 	SimpleDateFormat sdf;
1164 	if (time.indexOf('.') == -1) {
1165 	    // no '.', using format2
1166 	    sdf = new SimpleDateFormat(MDTM_TIME_FORMAT2);
1167 	} else {
1168 	    // using format1
1169 	    sdf = new SimpleDateFormat(MDTM_TIME_FORMAT1);
1170 	}
1171 	TimeZone tz = TimeZone.getTimeZone("GMT");
1172 	Calendar c = sdf.getCalendar();
1173 	c.setTimeZone(tz);
1174 	sdf.setCalendar(c);
1175 	Date d = sdf.parse(time);
1176 	return d;
1177     }
1178 
1179     /***
1180      * Specifies a file to be renamed. This command must be immediately followed
1181      * by a RNTO command. It is recommended that you use the rename()
1182      * convenience method rather than this one directly.
1183      * 
1184      * @param pathname
1185      *                the name of the file to be renamed
1186      * @exception IOException
1187      *                    caused by a socket IO error
1188      * @exception ServerResponseException
1189      *                    caused by a server response indicating an error
1190      * @exception FileNotFoundException
1191      *                    the given path does not exist
1192      */
1193     public synchronized void rnfr(String pathname) throws IOException,
1194 	    FileNotFoundException, ServerResponseException {
1195 	ostream.write("rnfr " + pathname + "\r\n");
1196 	ostream.flush();
1197 	log.debug("-> rnfr " + pathname);
1198 
1199 	String response = readResponse(istream);
1200 	log.debug(response);
1201 
1202 	StringTokenizer st = new StringTokenizer(response);
1203 	String return_code = st.nextToken();
1204 	if (!return_code.equals("350")) {
1205 	    if (return_code.equals("550")) {
1206 		throw new FileNotFoundException(response);
1207 	    }
1208 	    throw new ServerResponseException(response);
1209 	}
1210 	return;
1211     }
1212 
1213     /***
1214      * Renames a previously specified file to the given name. This command must
1215      * be immediately preceded by a RNFR command. It is recommended that you use
1216      * the rename() convenience method rather than this one directly.
1217      * 
1218      * @param pathname
1219      *                the name of the file to be renamed
1220      * @exception IOException
1221      *                    caused by a socket IO error
1222      * @exception ServerResponseException
1223      *                    caused by a server response indicating an error
1224      */
1225     public synchronized void rnto(String pathname) throws IOException,
1226 	    ServerResponseException {
1227 	ostream.write("rnto " + pathname + "\r\n");
1228 	ostream.flush();
1229 	log.debug("-> rnto " + pathname);
1230 
1231 	String response = readResponse(istream);
1232 	log.debug(response);
1233 
1234 	StringTokenizer st = new StringTokenizer(response);
1235 	String return_code = st.nextToken();
1236 	if (!return_code.equals("250")) {
1237 	    throw new ServerResponseException(response);
1238 	}
1239 	return;
1240     }
1241 
1242     /***
1243      * Returns the status of the named file or directory.
1244      * 
1245      * @param path
1246      *                the directory or file to get the status of, using a null
1247      *                path will cause this method to return the server status
1248      *                information
1249      * @exception IOException
1250      *                    caused by a socket IO error
1251      * @exception FileNotFoundException
1252      *                    the given path or file does not exist
1253      * @exception ServerResponseException
1254      *                    caused by a server response indicating an error
1255      * @return a Vector of String objects, each String being a single line of
1256      *         the status message from the server
1257      */
1258     public synchronized Vector stat(String path) throws IOException,
1259 	    FileNotFoundException, ServerResponseException {
1260 	String command;
1261 	if (path == null) {
1262 	    command = "stat\r\n";
1263 	} else {
1264 	    command = "stat " + path + "\r\n";
1265 	}
1266 	ostream.write(command);
1267 	ostream.flush();
1268 	log.debug("-> " + command);
1269 
1270 	String response = istream.readLine();
1271 	log.debug(response);
1272 
1273 	StringTokenizer st = new StringTokenizer(response, "- ");
1274 	String return_code = st.nextToken();
1275 	String remainder = response.substring(4);
1276 	if (return_code.equals("550")) {
1277 	    throw new FileNotFoundException(remainder);
1278 	}
1279 	if (!return_code.equals("211")) {
1280 	    if (response.charAt(3) == '-') {
1281 		// it's a multi-line response, read the whole response
1282 		String first_token;
1283 		boolean done = false;
1284 		while (!done) {
1285 		    response = istream.readLine();
1286 		    if (response == null) {
1287 			// end of input, bail
1288 			done = true;
1289 		    } else {
1290 			log.debug(response);
1291 			remainder = remainder + "\n" + response;
1292 			st = new StringTokenizer(response, " ");
1293 			first_token = st.nextToken();
1294 			if (return_code.equals(first_token)) {
1295 			    // last line, we're done
1296 			    done = true;
1297 			}
1298 		    }
1299 		}
1300 	    }
1301 	    // now indicate a protocol error
1302 	    throw new ServerResponseException(remainder);
1303 	}
1304 	// read and return the server reply
1305 	Vector status = new Vector();
1306 	if (response.charAt(3) == '-') {
1307 	    // it's a multi-line response, read the whole response
1308 	    String first_token;
1309 	    boolean done = false;
1310 	    while (!done) {
1311 		response = istream.readLine();
1312 		if (response == null) {
1313 		    // end of input, bail
1314 		    done = true;
1315 		} else {
1316 		    log.debug(response);
1317 		    st = new StringTokenizer(response, " ");
1318 		    first_token = st.nextToken();
1319 		    if (return_code.equals(first_token)) {
1320 			// last line, we're done
1321 			done = true;
1322 		    } else {
1323 			status.addElement(response);
1324 		    }
1325 		}
1326 	    }
1327 	}
1328 	return status;
1329     }
1330 
1331     /***
1332      * Returns the server status message. This is equivalent to using
1333      * stat(null).
1334      * 
1335      * @exception IOException
1336      *                    caused by a socket IO error
1337      * @exception ServerResponseException
1338      *                    caused by a server response indicating an error
1339      * @return a Vector of String objects, each String being a single line of
1340      *         the status message from the server
1341      */
1342     public synchronized Vector stat() throws IOException,
1343 	    ServerResponseException {
1344 	return stat(null);
1345     }
1346 
1347     /***
1348      * prepare for server-to-server transfer (passive mode)
1349      * 
1350      * @exception IOException
1351      *                    io error occurred
1352      * @exception ServerResponseException
1353      *                    server replied with an error code
1354      * @return tuple containing the server IP address and port number
1355      */
1356     public synchronized PassiveParameters pasv() throws IOException,
1357 	    ServerResponseException {
1358 	ostream.write("pasv\r\n");
1359 	ostream.flush();
1360 	log.debug("-> pasv");
1361 
1362 	// get reply
1363 	String response = readResponse(istream);
1364 	log.debug(response);
1365 
1366 	// check response
1367 	StringTokenizer st = new StringTokenizer(response);
1368 	if (!st.nextToken().equals("227")) {
1369 	    throw (new ServerResponseException(response));
1370 	}
1371 
1372 	// get ip-address & port returned
1373 	st.nextToken("(,)");
1374 	String addr = new String(st.nextToken());
1375 	addr = addr + "." + st.nextToken();
1376 	addr = addr + "." + st.nextToken();
1377 	addr = addr + "." + st.nextToken();
1378 
1379 	int p = (((Integer.parseInt(st.nextToken()) << 8) & 0xff00) | (Integer
1380 		.parseInt(st.nextToken()) & 0x00ff));
1381 	return new PassiveParameters(addr, p);
1382 
1383     }
1384 
1385     /***
1386      * end session
1387      * 
1388      * @exception IOException
1389      *                    io error occurred
1390      * @exception ServerResponseException
1391      *                    server replied with an error code
1392      */
1393     public synchronized void quit() throws IOException, ServerResponseException {
1394 	try {
1395 	    // send quit command to server
1396 	    ostream.write("quit\r\n");
1397 	    ostream.flush();
1398 	    log.debug("-> quit");
1399 
1400 	    // make sure command is accepted
1401 	    String response = readResponse(istream);
1402 	    log.debug(response);
1403 
1404 	    String temp = new String(response.substring(0, 3));
1405 	    if (!temp.equals("221")) { // looking for 'Goodbye' message
1406 		throw (new ServerResponseException(response));
1407 	    }
1408 	} finally {
1409 	    if (sock != null && !sock.isClosed()) {
1410 		try {
1411 		    log.debug("Setting socket to 0 lingering");
1412 		    sock.setSoLinger(true, 0);
1413 		    sock.close();
1414 		} catch (SocketException e) {
1415 		    // Don't care.
1416 		}
1417 	    }
1418 	}
1419     }
1420 
1421     // ***** data manipulation functions *****
1422 
1423     // return the greeting value
1424     public String getGreeting() {
1425 	return greeting;
1426     }
1427 
1428     // ***** protected methods *****
1429 
1430     /***
1431      * read a (multi-line) response
1432      * 
1433      * @param input
1434      *                the BufferedReader to read data from
1435      * @exception IOException
1436      *                    an IO error occurred
1437      * @return the response in a Vector of Strings
1438      */
1439     protected synchronized String readResponse(BufferedReader input)
1440 	    throws IOException {
1441 	String rc = "";
1442 	String response = "";
1443 
1444 	boolean done = false;
1445 	boolean first = true;
1446 
1447 	String tmp = null;
1448 	while (!done && (tmp = input.readLine()) != null) {
1449 	    response += tmp + '\n';
1450 	    if (tmp.length() >= 4) {
1451 		if (first == true) {
1452 		    // first time through
1453 		    rc = response.substring(0, 3);
1454 		    first = false;
1455 		}
1456 		// from rfc0959, a multiline response has '-' characters
1457 		// in the third position for all response lines other than
1458 		// the last which has a ' ' character after the response
1459 		// code number.
1460 		if ((rc.equals(tmp.substring(0, 3))) && (tmp.charAt(3) == ' ')) {
1461 		    done = true;
1462 		}
1463 	    } else {
1464 		// length is < 4
1465 		// not sure if this is a valid condition
1466 		// I assume this is ok and continue...
1467 	    }
1468 	}
1469 	return response;
1470     }
1471 
1472     // ***** private methods *****
1473 
1474     /***
1475      * connect to the given host at the given port number.
1476      * 
1477      * @param host
1478      *                the hostname of the server to connect to
1479      * @param portnumber
1480      *                the port that the server is listening on
1481      * @exception UnknownHostException
1482      *                    the given host name cannot be resolved
1483      * @exception IOException
1484      *                    an IO error occurred
1485      * @exception ServerResponseException
1486      *                    the server responded with an error
1487      */
1488     protected void connect(String host, int portnumber)
1489 	    throws UnknownHostException, IOException, ServerResponseException {
1490 
1491 	hylafaxServerHost = host;
1492 	hylafaxServerPort = portnumber;
1493 
1494 	SocketAddress sockAddr = new InetSocketAddress(host, portnumber);
1495 	sock = new Socket();
1496 	sock.connect(sockAddr, socketTimeout);
1497 	sock.setSoTimeout(socketTimeout);
1498 
1499 	// open streams for input and output to server
1500 	istream = new BufferedReader(new InputStreamReader(sock
1501 		.getInputStream()));
1502 
1503 	// for international users, the following MAY need to be changed
1504 	// to specify the "US-ASCII" encoding as the second parameter.
1505 	// someone will need to test this, hint hint
1506 	ostream = new OutputStreamWriter(sock.getOutputStream());
1507 
1508 	// get greeting line(s) of text from server
1509 	greeting = readResponse(istream);
1510 
1511 	// make sure response starts with "220" before continuing
1512 	String temp = new String(greeting.substring(0, 3));
1513 
1514 	// Log any connect warnings from hfaxd.
1515 	// There are two specifically we know are possibilities.
1516 	// 1. 130 Warning, no inverse address mapping for client host name
1517 	// 2. 130 Warning, client address is not listed for host name
1518 	while (temp.equals("130")) {
1519 	    log.warn(greeting);
1520 	    greeting = readResponse(istream);
1521 	    temp = new String(greeting.substring(0, 3));
1522 	}
1523 
1524 	if (!temp.equals("220")) {
1525 	    throw (new ServerResponseException(greeting));
1526 	}
1527 
1528 	port = portnumber;
1529     }
1530 
1531 }
1532 
1533 // FtpClientProtocol.java