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 }