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.ftp.FtpClient;
56 import sun.net.ftp.FtpProtocolException;
57 import sun.net.ProgressSource;
58 import sun.net.ProgressMonitor;
59 import sun.net.www.ParseUtil;
60 import 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  */
82 public class FtpURLConnection extends URLConnection {
83 
84 // Android-changed: Removed support for proxying FTP over HTTP since it
85 // relies on the removed sun.net.www.protocol.http.HttpURLConnection API.
86 //    // In case we have to use proxies, we use HttpURLConnection
87 //    HttpURLConnection http = null;
88     private Proxy instProxy;
89 
90     InputStream is = null;
91     OutputStream os = null;
92 
93     FtpClient ftp = null;
94     Permission permission;
95 
96     String password;
97     String user;
98 
99     String host;
100     String pathname;
101     String filename;
102     String fullpath;
103     int port;
104     static final int NONE = 0;
105     static final int ASCII = 1;
106     static final int BIN = 2;
107     static final int DIR = 3;
108     int type = NONE;
109     /* Redefine timeouts from java.net.URLConnection as we need -1 to mean
110      * not set. This is to ensure backward compatibility.
111      */
112     private int connectTimeout = NetworkClient.DEFAULT_CONNECT_TIMEOUT;;
113     private int readTimeout = NetworkClient.DEFAULT_READ_TIMEOUT;;
114 
115     /**
116      * For FTP URLs we need to have a special InputStream because we
117      * need to close 2 sockets after we're done with it :
118      *  - The Data socket (for the file).
119      *   - The command socket (FtpClient).
120      * Since that's the only class that needs to see that, it is an inner class.
121      */
122     protected class FtpInputStream extends FilterInputStream {
123         FtpClient ftp;
FtpInputStream(FtpClient cl, InputStream fd)124         FtpInputStream(FtpClient cl, InputStream fd) {
125             super(new BufferedInputStream(fd));
126             ftp = cl;
127         }
128 
129         @Override
close()130         public void close() throws IOException {
131             super.close();
132             if (ftp != null) {
133                 ftp.close();
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;
FtpOutputStream(FtpClient cl, OutputStream fd)147         FtpOutputStream(FtpClient cl, OutputStream fd) {
148             super(fd);
149             ftp = cl;
150         }
151 
152         @Override
close()153         public void close() throws IOException {
154             super.close();
155             if (ftp != null) {
156                 ftp.close();
157             }
158         }
159     }
160 
161     /**
162      * Creates an FtpURLConnection from a URL.
163      *
164      * @param   url     The <code>URL</code> to retrieve or store.
165      */
FtpURLConnection(URL url)166     public FtpURLConnection(URL url) throws IOException {
167         this(url, null);
168     }
169 
170     /**
171      * Same as FtpURLconnection(URL) with a per connection proxy specified
172      */
FtpURLConnection(URL url, Proxy p)173     FtpURLConnection(URL url, Proxy p) throws IOException {
174         super(url);
175         instProxy = p;
176         host = url.getHost();
177         port = url.getPort();
178         String userInfo = url.getUserInfo();
179 
180         if (!NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted()) {
181             // Cleartext network traffic is not permitted -- refuse this connection.
182             throw new IOException("Cleartext traffic not permitted: "
183                     + url.getProtocol() + "://" + host
184                     + ((url.getPort() >= 0) ? (":" + url.getPort()) : ""));
185         }
186 
187         if (userInfo != null) { // get the user and password
188             int delimiter = userInfo.indexOf(':');
189             if (delimiter == -1) {
190                 user = ParseUtil.decode(userInfo);
191                 password = null;
192             } else {
193                 user = ParseUtil.decode(userInfo.substring(0, delimiter++));
194                 password = ParseUtil.decode(userInfo.substring(delimiter));
195             }
196         }
197     }
198 
setTimeouts()199     private void setTimeouts() {
200         if (ftp != null) {
201             if (connectTimeout >= 0) {
202                 ftp.setConnectTimeout(connectTimeout);
203             }
204             if (readTimeout >= 0) {
205                 ftp.setReadTimeout(readTimeout);
206             }
207         }
208     }
209 
210     /**
211      * Connects to the FTP server and logs in.
212      *
213      * @throws  FtpLoginException if the login is unsuccessful
214      * @throws  FtpProtocolException if an error occurs
215      * @throws  UnknownHostException if trying to connect to an unknown host
216      */
217 
connect()218     public synchronized void connect() throws IOException {
219         if (connected) {
220             return;
221         }
222 
223         Proxy p = null;
224         if (instProxy == null) { // no per connection proxy specified
225             /**
226              * Do we have to use a proxy?
227              */
228             ProxySelector sel = java.security.AccessController.doPrivileged(
229                     new java.security.PrivilegedAction<ProxySelector>() {
230                         public ProxySelector run() {
231                             return ProxySelector.getDefault();
232                         }
233                     });
234             if (sel != null) {
235                 URI uri = sun.net.www.ParseUtil.toURI(url);
236                 Iterator<Proxy> it = sel.select(uri).iterator();
237                 while (it.hasNext()) {
238                     p = it.next();
239                     if (p == null || p == Proxy.NO_PROXY ||
240                         p.type() == Proxy.Type.SOCKS) {
241                         break;
242                     }
243                     if (p.type() != Proxy.Type.HTTP ||
244                             !(p.address() instanceof InetSocketAddress)) {
245                         sel.connectFailed(uri, p.address(), new IOException("Wrong proxy type"));
246                         continue;
247                     }
248                     // OK, we have an http proxy
249                     // Android-changed: Removed support for proxying FTP over HTTP since it
250                     // relies on the removed sun.net.www.protocol.http.HttpURLConnection API.
251                     sel.connectFailed(uri, p.address(), new IOException("FTP connections over HTTP proxy not supported"));
252                     continue;
253 //                    InetSocketAddress paddr = (InetSocketAddress) p.address();
254 //                    try {
255 //                        http = new HttpURLConnection(url, p);
256 //                        http.setDoInput(getDoInput());
257 //                        http.setDoOutput(getDoOutput());
258 //                        if (connectTimeout >= 0) {
259 //                            http.setConnectTimeout(connectTimeout);
260 //                        }
261 //                        if (readTimeout >= 0) {
262 //                            http.setReadTimeout(readTimeout);
263 //                        }
264 //                        http.connect();
265 //                        connected = true;
266 //                        return;
267 //                    } catch (IOException ioe) {
268 //                        sel.connectFailed(uri, paddr, ioe);
269 //                        http = null;
270 //                    }
271                 }
272             }
273         } else { // per connection proxy specified
274             p = instProxy;
275 // Android-changed: Removed support for proxying FTP over HTTP since it
276 // relies on the removed sun.net.www.protocol.http.HttpURLConnection API.
277 // As specified in the documentation for URL.openConnection(Proxy), we
278 // ignore the unsupported proxy and attempt a normal (direct) connection
279 //            if (p.type() == Proxy.Type.HTTP) {
280 //                http = new HttpURLConnection(url, instProxy);
281 //                http.setDoInput(getDoInput());
282 //                http.setDoOutput(getDoOutput());
283 //                if (connectTimeout >= 0) {
284 //                    http.setConnectTimeout(connectTimeout);
285 //                }
286 //                if (readTimeout >= 0) {
287 //                    http.setReadTimeout(readTimeout);
288 //                }
289 //                http.connect();
290 //                connected = true;
291 //                return;
292 //            }
293         }
294 
295         if (user == null) {
296             user = "anonymous";
297             String vers = java.security.AccessController.doPrivileged(
298                     new GetPropertyAction("java.version"));
299             password = java.security.AccessController.doPrivileged(
300                     new GetPropertyAction("ftp.protocol.user",
301                                           "Java" + vers + "@"));
302         }
303         try {
304             ftp = FtpClient.create();
305             if (p != null) {
306                 ftp.setProxy(p);
307             }
308             setTimeouts();
309             if (port != -1) {
310                 ftp.connect(new InetSocketAddress(host, port));
311             } else {
312                 ftp.connect(new InetSocketAddress(host, FtpClient.defaultPort()));
313             }
314         } catch (UnknownHostException e) {
315             // Maybe do something smart here, like use a proxy like iftp.
316             // Just keep throwing for now.
317             throw e;
318         } catch (FtpProtocolException fe) {
319             throw new IOException(fe);
320         }
321         try {
322             ftp.login(user, password.toCharArray());
323         } catch (sun.net.ftp.FtpProtocolException e) {
324             ftp.close();
325             // Backward compatibility
326             throw new sun.net.ftp.FtpLoginException("Invalid username/password");
327         }
328         connected = true;
329     }
330 
331 
332     /*
333      * Decodes the path as per the RFC-1738 specifications.
334      */
decodePath(String path)335     private void decodePath(String path) {
336         int i = path.indexOf(";type=");
337         if (i >= 0) {
338             String s1 = path.substring(i + 6, path.length());
339             if ("i".equalsIgnoreCase(s1)) {
340                 type = BIN;
341             }
342             if ("a".equalsIgnoreCase(s1)) {
343                 type = ASCII;
344             }
345             if ("d".equalsIgnoreCase(s1)) {
346                 type = DIR;
347             }
348             path = path.substring(0, i);
349         }
350         if (path != null && path.length() > 1 &&
351                 path.charAt(0) == '/') {
352             path = path.substring(1);
353         }
354         if (path == null || path.length() == 0) {
355             path = "./";
356         }
357         if (!path.endsWith("/")) {
358             i = path.lastIndexOf('/');
359             if (i > 0) {
360                 filename = path.substring(i + 1, path.length());
361                 filename = ParseUtil.decode(filename);
362                 pathname = path.substring(0, i);
363             } else {
364                 filename = ParseUtil.decode(path);
365                 pathname = null;
366             }
367         } else {
368             pathname = path.substring(0, path.length() - 1);
369             filename = null;
370         }
371         if (pathname != null) {
372             fullpath = pathname + "/" + (filename != null ? filename : "");
373         } else {
374             fullpath = filename;
375         }
376     }
377 
378     /*
379      * As part of RFC-1738 it is specified that the path should be
380      * interpreted as a series of FTP CWD commands.
381      * This is because, '/' is not necessarly the directory delimiter
382      * on every systems.
383      */
cd(String path)384     private void cd(String path) throws FtpProtocolException, IOException {
385         if (path == null || path.isEmpty()) {
386             return;
387         }
388         if (path.indexOf('/') == -1) {
389             ftp.changeDirectory(ParseUtil.decode(path));
390             return;
391         }
392 
393         StringTokenizer token = new StringTokenizer(path, "/");
394         while (token.hasMoreTokens()) {
395             ftp.changeDirectory(ParseUtil.decode(token.nextToken()));
396         }
397     }
398 
399     /**
400      * Get the InputStream to retreive the remote file. It will issue the
401      * "get" (or "dir") command to the ftp server.
402      *
403      * @return  the <code>InputStream</code> to the connection.
404      *
405      * @throws  IOException if already opened for output
406      * @throws  FtpProtocolException if errors occur during the transfert.
407      */
408     @Override
getInputStream()409     public InputStream getInputStream() throws IOException {
410         if (!connected) {
411             connect();
412         }
413         // Android-changed: Removed support for proxying FTP over HTTP since it
414         // relies on the removed sun.net.www.protocol.http.HttpURLConnection API.
415 //        if (http != null) {
416 //            return http.getInputStream();
417 //        }
418 
419         if (os != null) {
420             throw new IOException("Already opened for output");
421         }
422 
423         if (is != null) {
424             return is;
425         }
426 
427         MessageHeader msgh = new MessageHeader();
428 
429         boolean isAdir = false;
430         try {
431             decodePath(url.getPath());
432             if (filename == null || type == DIR) {
433                 ftp.setAsciiType();
434                 cd(pathname);
435                 if (filename == null) {
436                     is = new FtpInputStream(ftp, ftp.list(null));
437                 } else {
438                     is = new FtpInputStream(ftp, ftp.nameList(filename));
439                 }
440             } else {
441                 if (type == ASCII) {
442                     ftp.setAsciiType();
443                 } else {
444                     ftp.setBinaryType();
445                 }
446                 cd(pathname);
447                 is = new FtpInputStream(ftp, ftp.getFileStream(filename));
448             }
449 
450             /* Try to get the size of the file in bytes.  If that is
451             successful, then create a MeteredStream. */
452             try {
453                 long l = ftp.getLastTransferSize();
454                 msgh.add("content-length", Long.toString(l));
455                 if (l > 0) {
456 
457                     // Wrap input stream with MeteredStream to ensure read() will always return -1
458                     // at expected length.
459 
460                     // Check if URL should be metered
461                     boolean meteredInput = ProgressMonitor.getDefault().shouldMeterInput(url, "GET");
462                     ProgressSource pi = null;
463 
464                     if (meteredInput) {
465                         pi = new ProgressSource(url, "GET", l);
466                         pi.beginTracking();
467                     }
468 
469                     is = new MeteredStream(is, pi, l);
470                 }
471             } catch (Exception e) {
472                 e.printStackTrace();
473             /* do nothing, since all we were doing was trying to
474             get the size in bytes of the file */
475             }
476 
477             if (isAdir) {
478                 msgh.add("content-type", "text/plain");
479                 msgh.add("access-type", "directory");
480             } else {
481                 msgh.add("access-type", "file");
482                 String ftype = guessContentTypeFromName(fullpath);
483                 if (ftype == null && is.markSupported()) {
484                     ftype = guessContentTypeFromStream(is);
485                 }
486                 if (ftype != null) {
487                     msgh.add("content-type", ftype);
488                 }
489             }
490         } catch (FileNotFoundException e) {
491             try {
492                 cd(fullpath);
493                 /* if that worked, then make a directory listing
494                 and build an html stream with all the files in
495                 the directory */
496                 ftp.setAsciiType();
497 
498                 is = new FtpInputStream(ftp, ftp.list(null));
499                 msgh.add("content-type", "text/plain");
500                 msgh.add("access-type", "directory");
501             } catch (IOException ex) {
502                 throw new FileNotFoundException(fullpath);
503             } catch (FtpProtocolException ex2) {
504                 throw new FileNotFoundException(fullpath);
505             }
506         } catch (FtpProtocolException ftpe) {
507             throw new IOException(ftpe);
508         }
509         setProperties(msgh);
510         return is;
511     }
512 
513     /**
514      * Get the OutputStream to store the remote file. It will issue the
515      * "put" command to the ftp server.
516      *
517      * @return  the <code>OutputStream</code> to the connection.
518      *
519      * @throws  IOException if already opened for input or the URL
520      *          points to a directory
521      * @throws  FtpProtocolException if errors occur during the transfert.
522      */
523     @Override
getOutputStream()524     public OutputStream getOutputStream() throws IOException {
525         if (!connected) {
526             connect();
527         }
528 // Android-changed: Removed support for proxying FTP over HTTP since it
529 // relies on the removed sun.net.www.protocol.http.HttpURLConnection API.
530 //        if (http != null) {
531 //            OutputStream out = http.getOutputStream();
532 //            // getInputStream() is neccessary to force a writeRequests()
533 //            // on the http client.
534 //            http.getInputStream();
535 //            return out;
536 //        }
537 
538         if (is != null) {
539             throw new IOException("Already opened for input");
540         }
541 
542         if (os != null) {
543             return os;
544         }
545 
546         decodePath(url.getPath());
547         if (filename == null || filename.length() == 0) {
548             throw new IOException("illegal filename for a PUT");
549         }
550         try {
551             if (pathname != null) {
552                 cd(pathname);
553             }
554             if (type == ASCII) {
555                 ftp.setAsciiType();
556             } else {
557                 ftp.setBinaryType();
558             }
559             os = new FtpOutputStream(ftp, ftp.putFileStream(filename, false));
560         } catch (FtpProtocolException e) {
561             throw new IOException(e);
562         }
563         return os;
564     }
565 
guessContentTypeFromFilename(String fname)566     String guessContentTypeFromFilename(String fname) {
567         return guessContentTypeFromName(fname);
568     }
569 
570     /**
571      * Gets the <code>Permission</code> associated with the host & port.
572      *
573      * @return  The <code>Permission</code> object.
574      */
575     @Override
getPermission()576     public Permission getPermission() {
577         if (permission == null) {
578             int urlport = url.getPort();
579             urlport = urlport < 0 ? FtpClient.defaultPort() : urlport;
580             String urlhost = this.host + ":" + urlport;
581             permission = new SocketPermission(urlhost, "connect");
582         }
583         return permission;
584     }
585 
586     /**
587      * Sets the general request property. If a property with the key already
588      * exists, overwrite its value with the new value.
589      *
590      * @param   key     the keyword by which the request is known
591      *                  (e.g., "<code>accept</code>").
592      * @param   value   the value associated with it.
593      * @throws IllegalStateException if already connected
594      * @see #getRequestProperty(java.lang.String)
595      */
596     @Override
597     public void setRequestProperty(String key, String value) {
598         super.setRequestProperty(key, value);
599         if ("type".equals(key)) {
600             if ("i".equalsIgnoreCase(value)) {
601                 type = BIN;
602             } else if ("a".equalsIgnoreCase(value)) {
603                 type = ASCII;
604             } else if ("d".equalsIgnoreCase(value)) {
605                 type = DIR;
606             } else {
607                 throw new IllegalArgumentException(
608                         "Value of '" + key +
609                         "' request property was '" + value +
610                         "' when it must be either 'i', 'a' or 'd'");
611             }
612         }
613     }
614 
615     /**
616      * Returns the value of the named general request property for this
617      * connection.
618      *
619      * @param key the keyword by which the request is known (e.g., "accept").
620      * @return  the value of the named general request property for this
621      *           connection.
622      * @throws IllegalStateException if already connected
623      * @see #setRequestProperty(java.lang.String, java.lang.String)
624      */
625     @Override
626     public String getRequestProperty(String key) {
627         String value = super.getRequestProperty(key);
628 
629         if (value == null) {
630             if ("type".equals(key)) {
631                 value = (type == ASCII ? "a" : type == DIR ? "d" : "i");
632             }
633         }
634 
635         return value;
636     }
637 
638     @Override
639     public void setConnectTimeout(int timeout) {
640         if (timeout < 0) {
641             throw new IllegalArgumentException("timeouts can't be negative");
642         }
643         connectTimeout = timeout;
644     }
645 
646     @Override
647     public int getConnectTimeout() {
648         return (connectTimeout < 0 ? 0 : connectTimeout);
649     }
650 
651     @Override
652     public void setReadTimeout(int timeout) {
653         if (timeout < 0) {
654             throw new IllegalArgumentException("timeouts can't be negative");
655         }
656         readTimeout = timeout;
657     }
658 
659     @Override
660     public int getReadTimeout() {
661         return readTimeout < 0 ? 0 : readTimeout;
662     }
663 }
664