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 }