blob: 44473d421d829ee580fe1d3ada25a99493be81b4 [file] [log] [blame]
J. Duke319a3b92007-12-01 00:00:00 +00001/*
2 * Copyright 1994-2006 Sun Microsystems, Inc. All Rights Reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation. Sun designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Sun in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
22 * CA 95054 USA or visit www.sun.com if you need additional information or
23 * have any questions.
24 */
25
26/**
27 * FTP stream opener.
28 */
29
30package sun.net.www.protocol.ftp;
31
32import java.io.IOException;
33import java.io.InputStream;
34import java.io.OutputStream;
35import java.io.BufferedInputStream;
36import java.io.FilterInputStream;
37import java.io.FilterOutputStream;
38import java.io.FileNotFoundException;
39import java.net.URL;
40import java.net.URLStreamHandler;
41import java.net.SocketPermission;
42import java.net.UnknownHostException;
43import java.net.MalformedURLException;
44import java.net.InetSocketAddress;
45import java.net.URI;
46import java.net.Proxy;
47import java.net.ProxySelector;
48import java.util.StringTokenizer;
49import java.util.Iterator;
50import java.security.Permission;
51import sun.net.www.MessageHeader;
52import sun.net.www.MeteredStream;
53import sun.net.www.URLConnection;
54import sun.net.www.protocol.http.HttpURLConnection;
55import sun.net.ftp.FtpClient;
56import sun.net.ftp.FtpProtocolException;
57import sun.net.ProgressSource;
58import sun.net.ProgressMonitor;
59import sun.net.www.ParseUtil;
60import sun.security.action.GetPropertyAction;
61
62
63/**
64 * This class Opens an FTP input (or output) stream given a URL.
65 * It works as a one shot FTP transfer :
66 * <UL>
67 * <LI>Login</LI>
68 * <LI>Get (or Put) the file</LI>
69 * <LI>Disconnect</LI>
70 * </UL>
71 * You should not have to use it directly in most cases because all will be handled
72 * in a abstract layer. Here is an example of how to use the class :
73 * <P>
74 * <code>URL url = new URL("ftp://ftp.sun.com/pub/test.txt");<p>
75 * UrlConnection con = url.openConnection();<p>
76 * InputStream is = con.getInputStream();<p>
77 * ...<p>
78 * is.close();</code>
79 *
80 * @see sun.net.ftp.FtpClient
81 */
82public class FtpURLConnection extends URLConnection {
83
84 // In case we have to use proxies, we use HttpURLConnection
85 HttpURLConnection http = null;
86 private Proxy instProxy;
87 Proxy proxy = null;
88
89 InputStream is = null;
90 OutputStream os = null;
91
92 FtpClient ftp = null;
93 Permission permission;
94
95 String password;
96 String user;
97
98 String host;
99 String pathname;
100 String filename;
101 String fullpath;
102 int port;
103 static final int NONE = 0;
104 static final int ASCII = 1;
105 static final int BIN = 2;
106 static final int DIR = 3;
107 int type = NONE;
108 /* Redefine timeouts from java.net.URLConnection as we nee -1 to mean
109 * not set. This is to ensure backward compatibility.
110 */
111 private int connectTimeout = -1;
112 private int readTimeout = -1;
113
114 /**
115 * For FTP URLs we need to have a special InputStream because we
116 * need to close 2 sockets after we're done with it :
117 * - The Data socket (for the file).
118 * - The command socket (FtpClient).
119 * Since that's the only class that needs to see that, it is an inner class.
120 */
121 protected class FtpInputStream extends FilterInputStream {
122 FtpClient ftp;
123 FtpInputStream(FtpClient cl, InputStream fd) {
124 super(new BufferedInputStream(fd));
125 ftp = cl;
126 }
127
128 public void close() throws IOException {
129 super.close();
130 try {
131 if (ftp != null)
132 ftp.closeServer();
133 } catch (IOException ex) {
134 }
135 }
136 }
137
138 /**
139 * For FTP URLs we need to have a special OutputStream because we
140 * need to close 2 sockets after we're done with it :
141 * - The Data socket (for the file).
142 * - The command socket (FtpClient).
143 * Since that's the only class that needs to see that, it is an inner class.
144 */
145 protected class FtpOutputStream extends FilterOutputStream {
146 FtpClient ftp;
147 FtpOutputStream(FtpClient cl, OutputStream fd) {
148 super(fd);
149 ftp = cl;
150 }
151
152 public void close() throws IOException {
153 super.close();
154 try {
155 if (ftp != null)
156 ftp.closeServer();
157 } catch (IOException ex) {
158 }
159 }
160 }
161
162 /**
163 * Creates an FtpURLConnection from a URL.
164 *
165 * @param url The <code>URL</code> to retrieve or store.
166 */
167 public FtpURLConnection(URL url) {
168 this(url, null);
169 }
170
171 /**
172 * Same as FtpURLconnection(URL) with a per connection proxy specified
173 */
174 FtpURLConnection(URL url, Proxy p) {
175 super(url);
176 instProxy = p;
177 host = url.getHost();
178 port = url.getPort();
179 String userInfo = url.getUserInfo();
180
181 if (userInfo != null) { // get the user and password
182 int delimiter = userInfo.indexOf(':');
183 if (delimiter == -1) {
184 user = ParseUtil.decode(userInfo);
185 password = null;
186 } else {
187 user = ParseUtil.decode(userInfo.substring(0, delimiter++));
188 password = ParseUtil.decode(userInfo.substring(delimiter));
189 }
190 }
191 }
192
193 private void setTimeouts() {
194 if (ftp != null) {
195 if (connectTimeout >= 0)
196 ftp.setConnectTimeout(connectTimeout);
197 if (readTimeout >= 0)
198 ftp.setReadTimeout(readTimeout);
199 }
200 }
201
202 /**
203 * Connects to the FTP server and logs in.
204 *
205 * @throws FtpLoginException if the login is unsuccessful
206 * @throws FtpProtocolException if an error occurs
207 * @throws UnknownHostException if trying to connect to an unknown host
208 */
209
210 public synchronized void connect() throws IOException {
211 if (connected) {
212 return;
213 }
214
215 Proxy p = null;
216 if (instProxy == null) { // no per connection proxy specified
217 /**
218 * Do we have to use a proxie?
219 */
220 ProxySelector sel = (ProxySelector)
221 java.security.AccessController.doPrivileged(
222 new java.security.PrivilegedAction() {
223 public Object run() {
224 return ProxySelector.getDefault();
225 }
226 });
227 if (sel != null) {
228 URI uri = sun.net.www.ParseUtil.toURI(url);
229 Iterator<Proxy> it = sel.select(uri).iterator();
230 while (it.hasNext()) {
231 p = it.next();
232 if (p == null || p == Proxy.NO_PROXY ||
233 p.type() == Proxy.Type.SOCKS)
234 break;
235 if (p.type() != Proxy.Type.HTTP ||
236 !(p.address() instanceof InetSocketAddress)) {
237 sel.connectFailed(uri, p.address(), new IOException("Wrong proxy type"));
238 continue;
239 }
240 // OK, we have an http proxy
241 InetSocketAddress paddr = (InetSocketAddress) p.address();
242 try {
243 http = new HttpURLConnection(url, p);
244 if (connectTimeout >= 0)
245 http.setConnectTimeout(connectTimeout);
246 if (readTimeout >= 0)
247 http.setReadTimeout(readTimeout);
248 http.connect();
249 connected = true;
250 return;
251 } catch (IOException ioe) {
252 sel.connectFailed(uri, paddr, ioe);
253 http = null;
254 }
255 }
256 }
257 } else { // per connection proxy specified
258 p = instProxy;
259 if (p.type() == Proxy.Type.HTTP) {
260 http = new HttpURLConnection(url, instProxy);
261 if (connectTimeout >= 0)
262 http.setConnectTimeout(connectTimeout);
263 if (readTimeout >= 0)
264 http.setReadTimeout(readTimeout);
265 http.connect();
266 connected = true;
267 return;
268 }
269 }
270
271 if (user == null) {
272 user = "anonymous";
273 String vers = java.security.AccessController.doPrivileged(
274 new GetPropertyAction("java.version"));
275 password = java.security.AccessController.doPrivileged(
276 new GetPropertyAction("ftp.protocol.user",
277 "Java" + vers +"@"));
278 }
279 try {
280 if (p != null)
281 ftp = new FtpClient(p);
282 else
283 ftp = new FtpClient();
284 setTimeouts();
285 if (port != -1)
286 ftp.openServer(host, port);
287 else
288 ftp.openServer(host);
289 } catch (UnknownHostException e) {
290 // Maybe do something smart here, like use a proxy like iftp.
291 // Just keep throwing for now.
292 throw e;
293 }
294 try {
295 ftp.login(user, password);
296 } catch (sun.net.ftp.FtpLoginException e) {
297 ftp.closeServer();
298 throw e;
299 }
300 connected = true;
301 }
302
303
304 /*
305 * Decodes the path as per the RFC-1738 specifications.
306 */
307 private void decodePath(String path) {
308 int i = path.indexOf(";type=");
309 if (i >= 0) {
310 String s1 = path.substring(i+6, path.length());
311 if ("i".equalsIgnoreCase(s1))
312 type = BIN;
313 if ("a".equalsIgnoreCase(s1))
314 type = ASCII;
315 if ("d".equalsIgnoreCase(s1))
316 type = DIR;
317 path = path.substring(0, i);
318 }
319 if (path != null && path.length() > 1 &&
320 path.charAt(0) == '/')
321 path = path.substring(1);
322 if (path == null || path.length() == 0)
323 path = "./";
324 if (!path.endsWith("/")) {
325 i = path.lastIndexOf('/');
326 if (i > 0) {
327 filename = path.substring(i+1, path.length());
328 filename = ParseUtil.decode(filename);
329 pathname = path.substring(0, i);
330 } else {
331 filename = ParseUtil.decode(path);
332 pathname = null;
333 }
334 } else {
335 pathname = path.substring(0, path.length() - 1);
336 filename = null;
337 }
338 if (pathname != null)
339 fullpath = pathname + "/" + (filename != null ? filename : "");
340 else
341 fullpath = filename;
342 }
343
344 /*
345 * As part of RFC-1738 it is specified that the path should be
346 * interpreted as a series of FTP CWD commands.
347 * This is because, '/' is not necessarly the directory delimiter
348 * on every systems.
349 */
350
351 private void cd(String path) throws IOException {
352 if (path == null || "".equals(path))
353 return;
354 if (path.indexOf('/') == -1) {
355 ftp.cd(ParseUtil.decode(path));
356 return;
357 }
358
359 StringTokenizer token = new StringTokenizer(path,"/");
360 while (token.hasMoreTokens())
361 ftp.cd(ParseUtil.decode(token.nextToken()));
362 }
363
364 /**
365 * Get the InputStream to retreive the remote file. It will issue the
366 * "get" (or "dir") command to the ftp server.
367 *
368 * @return the <code>InputStream</code> to the connection.
369 *
370 * @throws IOException if already opened for output
371 * @throws FtpProtocolException if errors occur during the transfert.
372 */
373 public InputStream getInputStream() throws IOException {
374 if (!connected) {
375 connect();
376 }
377
378 if (http != null)
379 return http.getInputStream();
380
381 if (os != null)
382 throw new IOException("Already opened for output");
383
384 if (is != null) {
385 return is;
386 }
387
388 MessageHeader msgh = new MessageHeader();
389
390 try {
391 decodePath(url.getPath());
392 if (filename == null || type == DIR) {
393 ftp.ascii();
394 cd(pathname);
395 if (filename == null)
396 is = new FtpInputStream(ftp, ftp.list());
397 else
398 is = new FtpInputStream(ftp, ftp.nameList(filename));
399 } else {
400 if (type == ASCII)
401 ftp.ascii();
402 else
403 ftp.binary();
404 cd(pathname);
405 is = new FtpInputStream(ftp, ftp.get(filename));
406 }
407
408 /* Try to get the size of the file in bytes. If that is
409 successful, then create a MeteredStream. */
410 try {
411 String response = ftp.getResponseString();
412 int offset;
413
414 if ((offset = response.indexOf(" bytes)")) != -1) {
415 int i = offset;
416 int c;
417
418 while (--i >= 0 && ((c = response.charAt(i)) >= '0'
419 && c <= '9'))
420 ;
421 long l = Long.parseLong(response.substring(i + 1, offset));
422 msgh.add("content-length", Long.toString(l));
423 if (l > 0) {
424
425 // Wrap input stream with MeteredStream to ensure read() will always return -1
426 // at expected length.
427
428 // Check if URL should be metered
429 boolean meteredInput = ProgressMonitor.getDefault().shouldMeterInput(url, "GET");
430 ProgressSource pi = null;
431
432 if (meteredInput) {
433 pi = new ProgressSource(url, "GET", l);
434 pi.beginTracking();
435 }
436
437 is = new MeteredStream(is, pi, l);
438 }
439 }
440 } catch (Exception e) {
441 e.printStackTrace();
442 /* do nothing, since all we were doing was trying to
443 get the size in bytes of the file */
444 }
445
446 String type = guessContentTypeFromName(fullpath);
447 if (type == null && is.markSupported()) {
448 type = guessContentTypeFromStream(is);
449 }
450 if (type != null) {
451 msgh.add("content-type", type);
452 }
453 } catch (FileNotFoundException e) {
454 try {
455 cd(fullpath);
456 /* if that worked, then make a directory listing
457 and build an html stream with all the files in
458 the directory */
459 ftp.ascii();
460
461 is = new FtpInputStream(ftp, ftp.list());
462 msgh.add("content-type", "text/plain");
463 } catch (IOException ex) {
464 throw new FileNotFoundException(fullpath);
465 }
466 }
467 setProperties(msgh);
468 return is;
469 }
470
471 /**
472 * Get the OutputStream to store the remote file. It will issue the
473 * "put" command to the ftp server.
474 *
475 * @return the <code>OutputStream</code> to the connection.
476 *
477 * @throws IOException if already opened for input or the URL
478 * points to a directory
479 * @throws FtpProtocolException if errors occur during the transfert.
480 */
481 public OutputStream getOutputStream() throws IOException {
482 if (!connected) {
483 connect();
484 }
485
486 if (http != null)
487 return http.getOutputStream();
488
489 if (is != null)
490 throw new IOException("Already opened for input");
491
492 if (os != null) {
493 return os;
494 }
495
496 decodePath(url.getPath());
497 if (filename == null || filename.length() == 0)
498 throw new IOException("illegal filename for a PUT");
499 if (pathname != null)
500 cd(pathname);
501 if (type == ASCII)
502 ftp.ascii();
503 else
504 ftp.binary();
505 os = new FtpOutputStream(ftp, ftp.put(filename));
506 return os;
507 }
508
509 String guessContentTypeFromFilename(String fname) {
510 return guessContentTypeFromName(fname);
511 }
512
513 /**
514 * Gets the <code>Permission</code> associated with the host & port.
515 *
516 * @return The <code>Permission</code> object.
517 */
518 public Permission getPermission() {
519 if (permission == null) {
520 int port = url.getPort();
521 port = port < 0 ? FtpClient.FTP_PORT : port;
522 String host = this.host + ":" + port;
523 permission = new SocketPermission(host, "connect");
524 }
525 return permission;
526 }
527
528 /**
529 * Sets the general request property. If a property with the key already
530 * exists, overwrite its value with the new value.
531 *
532 * @param key the keyword by which the request is known
533 * (e.g., "<code>accept</code>").
534 * @param value the value associated with it.
535 * @throws IllegalStateException if already connected
536 * @see #getRequestProperty(java.lang.String)
537 */
538 public void setRequestProperty(String key, String value) {
539 super.setRequestProperty (key, value);
540 if ("type".equals (key)) {
541 if ("i".equalsIgnoreCase(value))
542 type = BIN;
543 else if ("a".equalsIgnoreCase(value))
544 type = ASCII;
545 else
546 if ("d".equalsIgnoreCase(value))
547 type = DIR;
548 else
549 throw new IllegalArgumentException(
550 "Value of '" + key +
551 "' request property was '" + value +
552 "' when it must be either 'i', 'a' or 'd'");
553 }
554 }
555
556 /**
557 * Returns the value of the named general request property for this
558 * connection.
559 *
560 * @param key the keyword by which the request is known (e.g., "accept").
561 * @return the value of the named general request property for this
562 * connection.
563 * @throws IllegalStateException if already connected
564 * @see #setRequestProperty(java.lang.String, java.lang.String)
565 */
566 public String getRequestProperty(String key) {
567 String value = super.getRequestProperty (key);
568
569 if (value == null) {
570 if ("type".equals (key))
571 value = (type == ASCII ? "a" : type == DIR ? "d" : "i");
572 }
573
574 return value;
575 }
576
577 public void setConnectTimeout(int timeout) {
578 if (timeout < 0)
579 throw new IllegalArgumentException("timeouts can't be negative");
580 connectTimeout = timeout;
581 }
582
583 public int getConnectTimeout() {
584 return (connectTimeout < 0 ? 0 : connectTimeout);
585 }
586
587 public void setReadTimeout(int timeout) {
588 if (timeout < 0)
589 throw new IllegalArgumentException("timeouts can't be negative");
590 readTimeout = timeout;
591 }
592
593 public int getReadTimeout() {
594 return readTimeout < 0 ? 0 : readTimeout;
595 }
596}