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