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