Home | History | Annotate | Download | only in ftp
      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;
    124         FtpInputStream(FtpClient cl, InputStream fd) {
    125             super(new BufferedInputStream(fd));
    126             ftp = cl;
    127         }
    128 
    129         @Override
    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;
    147         FtpOutputStream(FtpClient cl, OutputStream fd) {
    148             super(fd);
    149             ftp = cl;
    150         }
    151 
    152         @Override
    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      */
    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      */
    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 
    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 
    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      */
    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      */
    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
    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
    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 
    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
    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