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