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