FtpURLConnection.java revision 2c87ad3a45cecf9e344487cad1abfdebe79f2c7c
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
31package sun.net.www.protocol.ftp;
32
33import java.io.IOException;
34import java.io.InputStream;
35import java.io.OutputStream;
36import java.io.BufferedInputStream;
37import java.io.FilterInputStream;
38import java.io.FilterOutputStream;
39import java.io.FileNotFoundException;
40import java.net.URL;
41import java.net.SocketPermission;
42import java.net.UnknownHostException;
43import java.net.InetSocketAddress;
44import java.net.URI;
45import java.net.Proxy;
46import java.net.ProxySelector;
47import java.util.StringTokenizer;
48import java.util.Iterator;
49import java.security.Permission;
50import libcore.net.NetworkSecurityPolicy;
51import sun.net.NetworkClient;
52import sun.net.www.MessageHeader;
53import sun.net.www.MeteredStream;
54import sun.net.www.URLConnection;
55import sun.net.www.protocol.http.HttpURLConnection;
56import sun.net.ftp.FtpClient;
57import sun.net.ftp.FtpProtocolException;
58import sun.net.ProgressSource;
59import sun.net.ProgressMonitor;
60import sun.net.www.ParseUtil;
61import 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 */
83public 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;
123        FtpInputStream(FtpClient cl, InputStream fd) {
124            super(new BufferedInputStream(fd));
125            ftp = cl;
126        }
127
128        @Override
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;
146        FtpOutputStream(FtpClient cl, OutputStream fd) {
147            super(fd);
148            ftp = cl;
149        }
150
151        @Override
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     */
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     */
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
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
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     */
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     */
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
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
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
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
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