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 }