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 
  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 sun.net.NetworkClient;
  50 import sun.net.www.MessageHeader;
  51 import sun.net.www.MeteredStream;
  52 import sun.net.www.URLConnection;
  53 import sun.net.www.protocol.http.HttpURLConnection;
  54 import sun.net.ftp.FtpClient;
  55 import sun.net.ftp.FtpProtocolException;
  56 import sun.net.ProgressSource;
  57 import sun.net.ProgressMonitor;
  58 import sun.net.www.ParseUtil;
  59 import 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  */
  81 public 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 }