001: /**
002: *
003: * Java FTP client library.
004: *
005: * Copyright (C) 2000-2003 Enterprise Distributed Technologies Ltd
006: *
007: * www.enterprisedt.com
008: *
009: * This library is free software; you can redistribute it and/or
010: * modify it under the terms of the GNU Lesser General Public
011: * License as published by the Free Software Foundation; either
012: * version 2.1 of the License, or (at your option) any later version.
013: *
014: * This library is distributed in the hope that it will be useful,
015: * but WITHOUT ANY WARRANTY; without even the implied warranty of
016: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
017: * Lesser General Public License for more details.
018: *
019: * You should have received a copy of the GNU Lesser General Public
020: * License along with this library; if not, write to the Free Software
021: * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
022: *
023: * Bug fixes, suggestions and comments should be sent to bruce@enterprisedt.com
024: *
025: * Change Log:
026: *
027: * $Log: FTPControlSocket.java,v $
028: * Revision 1.1.1.1 2005/06/23 15:22:59 smontoro
029: * hipergate backend
030: *
031: * Revision 1.1 2004/02/07 03:15:20 hipergate
032: * v2.0 pre-alpha
033: *
034: * Revision 1.6 2003/05/31 14:53:44 bruceb
035: * 1.2.2 changes
036: *
037: * Revision 1.5 2003/01/29 22:46:08 bruceb
038: * minor changes
039: *
040: * Revision 1.4 2002/11/19 22:01:25 bruceb
041: * changes for 1.2
042: *
043: * Revision 1.3 2001/10/09 20:53:46 bruceb
044: * Active mode changes
045: *
046: * Revision 1.1 2001/10/05 14:42:04 bruceb
047: * moved from old project
048: *
049: *
050: */package com.enterprisedt.net.ftp;
051:
052: import java.io.IOException;
053: import java.io.PrintWriter;
054: import java.io.BufferedReader;
055: import java.io.InputStream;
056: import java.io.OutputStream;
057: import java.io.InputStreamReader;
058: import java.io.OutputStreamWriter;
059: import java.io.Writer;
060:
061: import java.net.Socket;
062: import java.net.ServerSocket;
063: import java.net.InetAddress;
064:
065: /**
066: * Supports client-side FTP operations
067: *
068: * @author Bruce Blackshaw
069: * @version $Revision: 1.1.1.1 $
070: *
071: */
072: public class FTPControlSocket {
073:
074: /**
075: * Revision control id
076: */
077: private static String cvsId = "@(#)$Id: FTPControlSocket.java,v 1.1.1.1 2005/06/23 15:22:59 smontoro Exp $";
078:
079: /**
080: * Standard FTP end of line sequence
081: */
082: static final String EOL = "\r\n";
083:
084: /**
085: * The control port number for FTP
086: */
087: static final int CONTROL_PORT = 21;
088:
089: /**
090: * Used to flag messages
091: */
092: private static final String DEBUG_ARROW = "---> ";
093:
094: /**
095: * Start of password message
096: */
097: private static final String PASSWORD_MESSAGE = DEBUG_ARROW + "PASS";
098:
099: /**
100: * Controls if responses sent back by the
101: * server are sent to assigned output stream
102: */
103: private boolean debugResponses = false;
104:
105: /**
106: * Output stream debug is written to,
107: * stdout by default
108: */
109: private PrintWriter log = new PrintWriter(System.out);
110:
111: /**
112: * The underlying socket.
113: */
114: private Socket controlSock = null;
115:
116: /**
117: * The write that writes to the control socket
118: */
119: private Writer writer = null;
120:
121: /**
122: * The reader that reads control data from the
123: * control socket
124: */
125: private BufferedReader reader = null;
126:
127: /**
128: * Constructor. Performs TCP connection and
129: * sets up reader/writer. Allows different control
130: * port to be used
131: *
132: * @param remoteHost Remote hostname
133: * @param controlPort port for control stream
134: * @param millis the length of the timeout, in milliseconds
135: * @param log the new logging stream
136: */
137: public FTPControlSocket(String remoteHost, int controlPort,
138: PrintWriter log, int timeout) throws IOException,
139: FTPException {
140:
141: setLogStream(log);
142:
143: // ensure we get debug from initial connection sequence
144: debugResponses(true);
145: controlSock = new Socket(remoteHost, controlPort);
146: setTimeout(timeout);
147: initStreams();
148: validateConnection();
149:
150: // switch off debug - user can switch on from this point
151: debugResponses(false);
152: }
153:
154: /**
155: * Constructor. Performs TCP connection and
156: * sets up reader/writer. Allows different control
157: * port to be used
158: *
159: * @param remoteAddr Remote inet address
160: * @param controlPort port for control stream
161: * @param millis the length of the timeout, in milliseconds
162: * @param log the new logging stream
163: */
164: public FTPControlSocket(InetAddress remoteAddr, int controlPort,
165: PrintWriter log, int timeout) throws IOException,
166: FTPException {
167:
168: setLogStream(log);
169:
170: // ensure we get debug from initial connection sequence
171: debugResponses(true);
172: controlSock = new Socket(remoteAddr, controlPort);
173: setTimeout(timeout);
174: initStreams();
175: validateConnection();
176:
177: // switch off debug - user can switch on from this point
178: debugResponses(false);
179: }
180:
181: /**
182: * Checks that the standard 220 reply is returned
183: * following the initiated connection
184: */
185: private void validateConnection() throws IOException, FTPException {
186:
187: String reply = readReply();
188: validateReply(reply, "220");
189: }
190:
191: /**
192: * Obtain the reader/writer streams for this
193: * connection
194: */
195: private void initStreams() throws IOException {
196:
197: // input stream
198: InputStream is = controlSock.getInputStream();
199: reader = new BufferedReader(new InputStreamReader(is));
200:
201: // output stream
202: OutputStream os = controlSock.getOutputStream();
203: writer = new OutputStreamWriter(os);
204: }
205:
206: /**
207: * Get the name of the remote host
208: *
209: * @return remote host name
210: */
211: String getRemoteHostName() {
212: InetAddress addr = controlSock.getInetAddress();
213: return addr.getHostName();
214: }
215:
216: /**
217: * Set the TCP timeout on the underlying control socket.
218: *
219: * If a timeout is set, then any operation which
220: * takes longer than the timeout value will be
221: * killed with a java.io.InterruptedException.
222: *
223: * @param millis The length of the timeout, in milliseconds
224: */
225: void setTimeout(int millis) throws IOException {
226:
227: if (controlSock == null)
228: throw new IllegalStateException(
229: "Failed to set timeout - no control socket");
230:
231: controlSock.setSoTimeout(millis);
232: }
233:
234: /**
235: * Quit this FTP session and clean up.
236: */
237: public void logout() throws IOException {
238:
239: if (log != null) {
240: log.flush();
241: log = null;
242: }
243:
244: IOException ex = null;
245: try {
246: writer.close();
247: } catch (IOException e) {
248: ex = e;
249: }
250: try {
251: reader.close();
252: } catch (IOException e) {
253: ex = e;
254: }
255: try {
256: controlSock.close();
257: } catch (IOException e) {
258: ex = e;
259: }
260: if (ex != null)
261: throw ex;
262: }
263:
264: /**
265: * Request a data socket be created on the
266: * server, connect to it and return our
267: * connected socket.
268: *
269: * @param active if true, create in active mode, else
270: * in passive mode
271: * @return connected data socket
272: */
273: FTPDataSocket createDataSocket(FTPConnectMode connectMode)
274: throws IOException, FTPException {
275:
276: if (connectMode == FTPConnectMode.ACTIVE) {
277: return new FTPDataSocket(createDataSocketActive());
278: } else { // PASV
279: return new FTPDataSocket(createDataSocketPASV());
280: }
281: }
282:
283: /**
284: * Request a data socket be created on the Client
285: * client on any free port, do not connect it to yet.
286: *
287: * @return not connected data socket
288: */
289: ServerSocket createDataSocketActive() throws IOException,
290: FTPException {
291:
292: // use any available port
293: ServerSocket socket = new ServerSocket(0);
294:
295: // get the local address to which the control socket is bound.
296: InetAddress localhost = controlSock.getLocalAddress();
297:
298: // send the PORT command to the server
299: setDataPort(localhost, (short) socket.getLocalPort());
300:
301: return socket;
302: }
303:
304: /**
305: * Helper method to convert a byte into an unsigned short value
306: *
307: * @param value value to convert
308: * @return the byte value as an unsigned short
309: */
310: private short toUnsignedShort(byte value) {
311: return (value < 0) ? (short) (value + 256) : (short) value;
312: }
313:
314: /**
315: * Convert a short into a byte array
316: *
317: * @param value value to convert
318: * @return a byte array
319: */
320: protected byte[] toByteArray(short value) {
321:
322: byte[] bytes = new byte[2];
323: bytes[0] = (byte) (value >> 8); // bits 1- 8
324: bytes[1] = (byte) (value & 0x00FF); // bits 9-16
325: return bytes;
326: }
327:
328: /**
329: * Sets the data port on the server, i.e. sends a PORT
330: * command
331: *
332: * @param host the local host the server will connect to
333: * @param portNo the port number to connect to
334: */
335: private void setDataPort(InetAddress host, short portNo)
336: throws IOException, FTPException {
337:
338: byte[] hostBytes = host.getAddress();
339: byte[] portBytes = toByteArray(portNo);
340:
341: // assemble the PORT command
342: String cmd = new StringBuffer("PORT ").append(
343: toUnsignedShort(hostBytes[0])).append(",").append(
344: toUnsignedShort(hostBytes[1])).append(",").append(
345: toUnsignedShort(hostBytes[2])).append(",").append(
346: toUnsignedShort(hostBytes[3])).append(",").append(
347: toUnsignedShort(portBytes[0])).append(",").append(
348: toUnsignedShort(portBytes[1])).toString();
349:
350: // send command and check reply
351: String reply = sendCommand(cmd);
352: validateReply(reply, "200");
353: }
354:
355: /**
356: * Request a data socket be created on the
357: * server, connect to it and return our
358: * connected socket.
359: *
360: * @return connected data socket
361: */
362: Socket createDataSocketPASV() throws IOException, FTPException {
363:
364: // PASSIVE command - tells the server to listen for
365: // a connection attempt rather than initiating it
366: String reply = sendCommand("PASV");
367: validateReply(reply, "227");
368:
369: // The reply to PASV is in the form:
370: // 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2).
371: // where h1..h4 are the IP address to connect and
372: // p1,p2 the port number
373: // Example:
374: // 227 Entering Passive Mode (128,3,122,1,15,87).
375: // NOTE: PASV command in IBM/Mainframe returns the string
376: // 227 Entering Passive Mode 128,3,122,1,15,87 (missing
377: // brackets)
378:
379: // extract the IP data string from between the brackets
380: int startIP = reply.indexOf('(');
381: int endIP = reply.indexOf(')');
382:
383: // allow for IBM missing brackets around IP address
384: if (startIP < 0 && endIP < 0) {
385: startIP = reply.toUpperCase().lastIndexOf("MODE") + 4;
386: endIP = reply.length();
387: }
388:
389: String ipData = reply.substring(startIP + 1, endIP);
390: int parts[] = new int[6];
391:
392: int len = ipData.length();
393: int partCount = 0;
394: StringBuffer buf = new StringBuffer();
395:
396: // loop thru and examine each char
397: for (int i = 0; i < len && partCount <= 6; i++) {
398:
399: char ch = ipData.charAt(i);
400: if (Character.isDigit(ch))
401: buf.append(ch);
402: else if (ch != ',') {
403: throw new FTPException("Malformed PASV reply: " + reply);
404: }
405:
406: // get the part
407: if (ch == ',' || i + 1 == len) { // at end or at separator
408: try {
409: parts[partCount++] = Integer.parseInt(buf
410: .toString());
411: buf.setLength(0);
412: } catch (NumberFormatException ex) {
413: throw new FTPException("Malformed PASV reply: "
414: + reply);
415: }
416: }
417: }
418:
419: // assemble the IP address
420: // we try connecting, so we don't bother checking digits etc
421: String ipAddress = parts[0] + "." + parts[1] + "." + parts[2]
422: + "." + parts[3];
423:
424: // assemble the port number
425: int port = (parts[4] << 8) + parts[5];
426:
427: // create the socket
428: return new Socket(ipAddress, port);
429: }
430:
431: /**
432: * Send a command to the FTP server and
433: * return the server's reply
434: *
435: * @return reply to the supplied command
436: */
437: String sendCommand(String command) throws IOException {
438:
439: log(DEBUG_ARROW + command);
440:
441: // send it
442: writer.write(command + EOL);
443: writer.flush();
444:
445: // and read the result
446: return readReply();
447: }
448:
449: /**
450: * Read the FTP server's reply to a previously
451: * issued command. RFC 959 states that a reply
452: * consists of the 3 digit code followed by text.
453: * The 3 digit code is followed by a hyphen if it
454: * is a muliline response, and the last line starts
455: * with the same 3 digit code.
456: *
457: * @return reply string
458: */
459: String readReply() throws IOException {
460:
461: String firstLine = reader.readLine();
462: if (firstLine == null || firstLine.length() == 0)
463: throw new IOException("Unexpected null reply received");
464:
465: StringBuffer reply = new StringBuffer(firstLine);
466:
467: log(reply.toString());
468:
469: String replyCode = reply.toString().substring(0, 3);
470:
471: // check for multiline response and build up
472: // the reply
473: if (reply.charAt(3) == '-') {
474:
475: boolean complete = false;
476: while (!complete) {
477: String line = reader.readLine();
478: if (line == null)
479: throw new IOException(
480: "Unexpected null reply received");
481:
482: log(line);
483:
484: if (line.length() > 3
485: && line.substring(0, 3).equals(replyCode)
486: && line.charAt(3) == ' ') {
487: reply.append(line.substring(3));
488: complete = true;
489: } else { // not the last line
490: reply.append(" ");
491: reply.append(line);
492: }
493: } // end while
494: } // end if
495: return reply.toString();
496: }
497:
498: /**
499: * Validate the response the host has supplied against the
500: * expected reply. If we get an unexpected reply we throw an
501: * exception, setting the message to that returned by the
502: * FTP server
503: *
504: * @param reply the entire reply string we received
505: * @param expectedReplyCode the reply we expected to receive
506: *
507: */
508: FTPReply validateReply(String reply, String expectedReplyCode)
509: throws IOException, FTPException {
510:
511: // all reply codes are 3 chars long
512: String replyCode = reply.substring(0, 3);
513: String replyText = reply.substring(4);
514: FTPReply replyObj = new FTPReply(replyCode, replyText);
515:
516: if (replyCode.equals(expectedReplyCode))
517: return replyObj;
518:
519: // if unexpected reply, throw an exception
520: throw new FTPException(replyText, replyCode);
521: }
522:
523: /**
524: * Validate the response the host has supplied against the
525: * expected reply. If we get an unexpected reply we throw an
526: * exception, setting the message to that returned by the
527: * FTP server
528: *
529: * @param reply the entire reply string we received
530: * @param expectedReplyCodes array of expected replies
531: * @return an object encapsulating the server's reply
532: *
533: */
534: FTPReply validateReply(String reply, String[] expectedReplyCodes)
535: throws IOException, FTPException {
536:
537: // all reply codes are 3 chars long
538: String replyCode = reply.substring(0, 3);
539: String replyText = reply.substring(4);
540:
541: FTPReply replyObj = new FTPReply(replyCode, replyText);
542:
543: for (int i = 0; i < expectedReplyCodes.length; i++)
544: if (replyCode.equals(expectedReplyCodes[i]))
545: return replyObj;
546:
547: // got this far, not recognised
548: throw new FTPException(replyText, replyCode);
549: }
550:
551: /**
552: * Switch debug of responses on or off
553: *
554: * @param on true if you wish to have responses to
555: * stdout, false otherwise
556: */
557: void debugResponses(boolean on) {
558: debugResponses = on;
559: }
560:
561: /**
562: * Set the logging stream, replacing
563: * stdout. If null log supplied, logging is
564: * switched off
565: *
566: * @param log the new logging stream
567: */
568: void setLogStream(PrintWriter log) {
569: if (log != null)
570: this .log = log;
571: else
572: debugResponses = false;
573: }
574:
575: /**
576: * Log a message, if logging is set up
577: *
578: * @param msg message to log
579: */
580: void log(String msg) {
581: if (debugResponses && log != null) {
582: if (!msg.startsWith(PASSWORD_MESSAGE))
583: log.println(msg);
584: else
585: log.println(PASSWORD_MESSAGE + " ********");
586: }
587: }
588:
589: }
|