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