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