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 
  30 package sun.net.www.protocol.ftp;
  31 
  32 import java.io.IOException;
  33 import java.io.InputStream;
  34 import java.io.OutputStream;
  35 import java.io.BufferedInputStream;
  36 import java.io.FilterInputStream;
  37 import java.io.FilterOutputStream;
  38 import java.io.FileNotFoundException;
  39 import java.net.URL;
  40 import java.net.SocketPermission;
  41 import java.net.UnknownHostException;
  42 import java.net.InetSocketAddress;
  43 import java.net.URI;
  44 import java.net.Proxy;
  45 import java.net.ProxySelector;
  46 import java.util.StringTokenizer;
  47 import java.util.Iterator;
  48 import java.security.Permission;
  49 import java.util.Properties;
  50 import sun.net.NetworkClient;
  51 import sun.net.www.MessageHeader;
  52 import sun.net.www.MeteredStream;
  53 import sun.net.www.URLConnection;
  54 import sun.net.www.protocol.http.HttpURLConnection;
  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  * <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  */
  83 public 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 }