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