1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * Copyright (c) 1994, 2010, Oracle and/or its affiliates. All rights reserved. 4 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 5 * 6 * This code is free software; you can redistribute it and/or modify it 7 * under the terms of the GNU General Public License version 2 only, as 8 * published by the Free Software Foundation. Oracle designates this 9 * particular file as subject to the "Classpath" exception as provided 10 * by Oracle in the LICENSE file that accompanied this code. 11 * 12 * This code is distributed in the hope that it will be useful, but WITHOUT 13 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 14 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 15 * version 2 for more details (a copy is included in the LICENSE file that 16 * accompanied this code). 17 * 18 * You should have received a copy of the GNU General Public License version 19 * 2 along with this work; if not, write to the Free Software Foundation, 20 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 21 * 22 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 23 * or visit www.oracle.com if you need additional information or have any 24 * questions. 25 */ 26 27 /** 28 * FTP stream opener. 29 */ 30 31 package sun.net.www.protocol.ftp; 32 33 import java.io.IOException; 34 import java.io.InputStream; 35 import java.io.OutputStream; 36 import java.io.BufferedInputStream; 37 import java.io.FilterInputStream; 38 import java.io.FilterOutputStream; 39 import java.io.FileNotFoundException; 40 import java.net.URL; 41 import java.net.SocketPermission; 42 import java.net.UnknownHostException; 43 import java.net.InetSocketAddress; 44 import java.net.URI; 45 import java.net.Proxy; 46 import java.net.ProxySelector; 47 import java.util.StringTokenizer; 48 import java.util.Iterator; 49 import java.security.Permission; 50 import libcore.net.NetworkSecurityPolicy; 51 import sun.net.NetworkClient; 52 import sun.net.www.MessageHeader; 53 import sun.net.www.MeteredStream; 54 import sun.net.www.URLConnection; 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 * <P> 74 * <code>URL url = new URL("ftp://ftp.sun.com/pub/test.txt");<p> 75 * UrlConnection con = url.openConnection();<p> 76 * InputStream is = con.getInputStream();<p> 77 * ...<p> 78 * is.close();</code> 79 * 80 * @see sun.net.ftp.FtpClient 81 */ 82 public class FtpURLConnection extends URLConnection { 83 84 // Android-changed: Removed support for proxying FTP over HTTP. 85 // It relies on the removed sun.net.www.protocol.http.HttpURLConnection API. 86 /* 87 // In case we have to use proxies, we use HttpURLConnection 88 HttpURLConnection http = null; 89 */ 90 private Proxy instProxy; 91 92 InputStream is = null; 93 OutputStream os = null; 94 95 FtpClient ftp = null; 96 Permission permission; 97 98 String password; 99 String user; 100 101 String host; 102 String pathname; 103 String filename; 104 String fullpath; 105 int port; 106 static final int NONE = 0; 107 static final int ASCII = 1; 108 static final int BIN = 2; 109 static final int DIR = 3; 110 int type = NONE; 111 /* Redefine timeouts from java.net.URLConnection as we need -1 to mean 112 * not set. This is to ensure backward compatibility. 113 */ 114 private int connectTimeout = NetworkClient.DEFAULT_CONNECT_TIMEOUT;; 115 private int readTimeout = NetworkClient.DEFAULT_READ_TIMEOUT;; 116 117 /** 118 * For FTP URLs we need to have a special InputStream because we 119 * need to close 2 sockets after we're done with it : 120 * - The Data socket (for the file). 121 * - The command socket (FtpClient). 122 * Since that's the only class that needs to see that, it is an inner class. 123 */ 124 protected class FtpInputStream extends FilterInputStream { 125 FtpClient ftp; FtpInputStream(FtpClient cl, InputStream fd)126 FtpInputStream(FtpClient cl, InputStream fd) { 127 super(new BufferedInputStream(fd)); 128 ftp = cl; 129 } 130 131 @Override close()132 public void close() throws IOException { 133 super.close(); 134 if (ftp != null) { 135 ftp.close(); 136 } 137 } 138 } 139 140 /** 141 * For FTP URLs we need to have a special OutputStream because we 142 * need to close 2 sockets after we're done with it : 143 * - The Data socket (for the file). 144 * - The command socket (FtpClient). 145 * Since that's the only class that needs to see that, it is an inner class. 146 */ 147 protected class FtpOutputStream extends FilterOutputStream { 148 FtpClient ftp; FtpOutputStream(FtpClient cl, OutputStream fd)149 FtpOutputStream(FtpClient cl, OutputStream fd) { 150 super(fd); 151 ftp = cl; 152 } 153 154 @Override close()155 public void close() throws IOException { 156 super.close(); 157 if (ftp != null) { 158 ftp.close(); 159 } 160 } 161 } 162 163 /** 164 * Creates an FtpURLConnection from a URL. 165 * 166 * @param url The <code>URL</code> to retrieve or store. 167 */ 168 // Android-changed: Ctors can throw IOException for NetworkSecurityPolicy enforcement. 169 // public FtpURLConnection(URL url) { FtpURLConnection(URL url)170 public FtpURLConnection(URL url) throws IOException { 171 this(url, null); 172 } 173 174 /** 175 * Same as FtpURLconnection(URL) with a per connection proxy specified 176 */ 177 // Android-changed: Ctors can throw IOException for NetworkSecurityPolicy enforcement. 178 // FtpURLConnection(URL url, Proxy p) { FtpURLConnection(URL url, Proxy p)179 FtpURLConnection(URL url, Proxy p) throws IOException { 180 super(url); 181 instProxy = p; 182 host = url.getHost(); 183 port = url.getPort(); 184 String userInfo = url.getUserInfo(); 185 // BEGIN Android-added: Enforce NetworkSecurityPolicy.isClearTextTrafficPermitted(). 186 if (!NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted()) { 187 // Cleartext network traffic is not permitted -- refuse this connection. 188 throw new IOException("Cleartext traffic not permitted: " 189 + url.getProtocol() + "://" + host 190 + ((url.getPort() >= 0) ? (":" + url.getPort()) : "")); 191 } 192 // END Android-added: Enforce NetworkSecurityPolicy.isClearTextTrafficPermitted(). 193 194 if (userInfo != null) { // get the user and password 195 int delimiter = userInfo.indexOf(':'); 196 if (delimiter == -1) { 197 user = ParseUtil.decode(userInfo); 198 password = null; 199 } else { 200 user = ParseUtil.decode(userInfo.substring(0, delimiter++)); 201 password = ParseUtil.decode(userInfo.substring(delimiter)); 202 } 203 } 204 } 205 setTimeouts()206 private void setTimeouts() { 207 if (ftp != null) { 208 if (connectTimeout >= 0) { 209 ftp.setConnectTimeout(connectTimeout); 210 } 211 if (readTimeout >= 0) { 212 ftp.setReadTimeout(readTimeout); 213 } 214 } 215 } 216 217 /** 218 * Connects to the FTP server and logs in. 219 * 220 * @throws FtpLoginException if the login is unsuccessful 221 * @throws FtpProtocolException if an error occurs 222 * @throws UnknownHostException if trying to connect to an unknown host 223 */ 224 connect()225 public synchronized void connect() throws IOException { 226 if (connected) { 227 return; 228 } 229 230 Proxy p = null; 231 if (instProxy == null) { // no per connection proxy specified 232 /** 233 * Do we have to use a proxy? 234 */ 235 ProxySelector sel = java.security.AccessController.doPrivileged( 236 new java.security.PrivilegedAction<ProxySelector>() { 237 public ProxySelector run() { 238 return ProxySelector.getDefault(); 239 } 240 }); 241 if (sel != null) { 242 URI uri = sun.net.www.ParseUtil.toURI(url); 243 Iterator<Proxy> it = sel.select(uri).iterator(); 244 while (it.hasNext()) { 245 p = it.next(); 246 if (p == null || p == Proxy.NO_PROXY || 247 p.type() == Proxy.Type.SOCKS) { 248 break; 249 } 250 if (p.type() != Proxy.Type.HTTP || 251 !(p.address() instanceof InetSocketAddress)) { 252 sel.connectFailed(uri, p.address(), new IOException("Wrong proxy type")); 253 continue; 254 } 255 // OK, we have an http proxy 256 // BEGIN Android-changed: Removed support for proxying FTP over HTTP. 257 // It relies on the removed sun.net.www.protocol.http.HttpURLConnection API. 258 /* 259 InetSocketAddress paddr = (InetSocketAddress) p.address(); 260 try { 261 http = new HttpURLConnection(url, p); 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 } catch (IOException ioe) { 274 sel.connectFailed(uri, paddr, ioe); 275 http = null; 276 } 277 */ 278 sel.connectFailed(uri, p.address(), new IOException("FTP connections over HTTP proxy not supported")); 279 continue; 280 // END Android-changed: Removed support for proxying FTP over HTTP. 281 } 282 } 283 } else { // per connection proxy specified 284 p = instProxy; 285 // BEGIN Android-changed: Removed support for proxying FTP over HTTP. 286 // It relies on the removed sun.net.www.protocol.http.HttpURLConnection API. 287 // As specified in the documentation for URL.openConnection(Proxy), we 288 // ignore the unsupported proxy and attempt a normal (direct) connection 289 /* 290 if (p.type() == Proxy.Type.HTTP) { 291 http = new HttpURLConnection(url, instProxy); 292 http.setDoInput(getDoInput()); 293 http.setDoOutput(getDoOutput()); 294 if (connectTimeout >= 0) { 295 http.setConnectTimeout(connectTimeout); 296 } 297 if (readTimeout >= 0) { 298 http.setReadTimeout(readTimeout); 299 } 300 http.connect(); 301 connected = true; 302 return; 303 } 304 */ 305 // END Android-changed: Removed support for proxying FTP over HTTP. 306 } 307 308 if (user == null) { 309 user = "anonymous"; 310 String vers = java.security.AccessController.doPrivileged( 311 new GetPropertyAction("java.version")); 312 password = java.security.AccessController.doPrivileged( 313 new GetPropertyAction("ftp.protocol.user", 314 "Java" + vers + "@")); 315 } 316 try { 317 ftp = FtpClient.create(); 318 if (p != null) { 319 ftp.setProxy(p); 320 } 321 setTimeouts(); 322 if (port != -1) { 323 ftp.connect(new InetSocketAddress(host, port)); 324 } else { 325 ftp.connect(new InetSocketAddress(host, FtpClient.defaultPort())); 326 } 327 } catch (UnknownHostException e) { 328 // Maybe do something smart here, like use a proxy like iftp. 329 // Just keep throwing for now. 330 throw e; 331 } catch (FtpProtocolException fe) { 332 throw new IOException(fe); 333 } 334 try { 335 ftp.login(user, password == null ? null : password.toCharArray()); 336 } catch (sun.net.ftp.FtpProtocolException e) { 337 ftp.close(); 338 // Backward compatibility 339 throw new sun.net.ftp.FtpLoginException("Invalid username/password"); 340 } 341 connected = true; 342 } 343 344 345 /* 346 * Decodes the path as per the RFC-1738 specifications. 347 */ decodePath(String path)348 private void decodePath(String path) { 349 int i = path.indexOf(";type="); 350 if (i >= 0) { 351 String s1 = path.substring(i + 6, path.length()); 352 if ("i".equalsIgnoreCase(s1)) { 353 type = BIN; 354 } 355 if ("a".equalsIgnoreCase(s1)) { 356 type = ASCII; 357 } 358 if ("d".equalsIgnoreCase(s1)) { 359 type = DIR; 360 } 361 path = path.substring(0, i); 362 } 363 if (path != null && path.length() > 1 && 364 path.charAt(0) == '/') { 365 path = path.substring(1); 366 } 367 if (path == null || path.length() == 0) { 368 path = "./"; 369 } 370 if (!path.endsWith("/")) { 371 i = path.lastIndexOf('/'); 372 if (i > 0) { 373 filename = path.substring(i + 1, path.length()); 374 filename = ParseUtil.decode(filename); 375 pathname = path.substring(0, i); 376 } else { 377 filename = ParseUtil.decode(path); 378 pathname = null; 379 } 380 } else { 381 pathname = path.substring(0, path.length() - 1); 382 filename = null; 383 } 384 if (pathname != null) { 385 fullpath = pathname + "/" + (filename != null ? filename : ""); 386 } else { 387 fullpath = filename; 388 } 389 } 390 391 /* 392 * As part of RFC-1738 it is specified that the path should be 393 * interpreted as a series of FTP CWD commands. 394 * This is because, '/' is not necessarly the directory delimiter 395 * on every systems. 396 */ cd(String path)397 private void cd(String path) throws FtpProtocolException, IOException { 398 if (path == null || path.isEmpty()) { 399 return; 400 } 401 if (path.indexOf('/') == -1) { 402 ftp.changeDirectory(ParseUtil.decode(path)); 403 return; 404 } 405 406 StringTokenizer token = new StringTokenizer(path, "/"); 407 while (token.hasMoreTokens()) { 408 ftp.changeDirectory(ParseUtil.decode(token.nextToken())); 409 } 410 } 411 412 /** 413 * Get the InputStream to retreive the remote file. It will issue the 414 * "get" (or "dir") command to the ftp server. 415 * 416 * @return the <code>InputStream</code> to the connection. 417 * 418 * @throws IOException if already opened for output 419 * @throws FtpProtocolException if errors occur during the transfert. 420 */ 421 @Override getInputStream()422 public InputStream getInputStream() throws IOException { 423 if (!connected) { 424 connect(); 425 } 426 427 // Android-changed: Removed support for proxying FTP over HTTP. 428 // It relies on the removed sun.net.www.protocol.http.HttpURLConnection API. 429 /* 430 if (http != null) { 431 return http.getInputStream(); 432 } 433 */ 434 435 if (os != null) { 436 throw new IOException("Already opened for output"); 437 } 438 439 if (is != null) { 440 return is; 441 } 442 443 MessageHeader msgh = new MessageHeader(); 444 445 boolean isAdir = false; 446 try { 447 decodePath(url.getPath()); 448 if (filename == null || type == DIR) { 449 ftp.setAsciiType(); 450 cd(pathname); 451 if (filename == null) { 452 is = new FtpInputStream(ftp, ftp.list(null)); 453 } else { 454 is = new FtpInputStream(ftp, ftp.nameList(filename)); 455 } 456 } else { 457 if (type == ASCII) { 458 ftp.setAsciiType(); 459 } else { 460 ftp.setBinaryType(); 461 } 462 cd(pathname); 463 is = new FtpInputStream(ftp, ftp.getFileStream(filename)); 464 } 465 466 /* Try to get the size of the file in bytes. If that is 467 successful, then create a MeteredStream. */ 468 try { 469 long l = ftp.getLastTransferSize(); 470 msgh.add("content-length", Long.toString(l)); 471 if (l > 0) { 472 473 // Wrap input stream with MeteredStream to ensure read() will always return -1 474 // at expected length. 475 476 // Check if URL should be metered 477 boolean meteredInput = ProgressMonitor.getDefault().shouldMeterInput(url, "GET"); 478 ProgressSource pi = null; 479 480 if (meteredInput) { 481 pi = new ProgressSource(url, "GET", l); 482 pi.beginTracking(); 483 } 484 485 is = new MeteredStream(is, pi, l); 486 } 487 } catch (Exception e) { 488 e.printStackTrace(); 489 /* do nothing, since all we were doing was trying to 490 get the size in bytes of the file */ 491 } 492 493 if (isAdir) { 494 msgh.add("content-type", "text/plain"); 495 msgh.add("access-type", "directory"); 496 } else { 497 msgh.add("access-type", "file"); 498 String ftype = guessContentTypeFromName(fullpath); 499 if (ftype == null && is.markSupported()) { 500 ftype = guessContentTypeFromStream(is); 501 } 502 if (ftype != null) { 503 msgh.add("content-type", ftype); 504 } 505 } 506 } catch (FileNotFoundException e) { 507 try { 508 cd(fullpath); 509 /* if that worked, then make a directory listing 510 and build an html stream with all the files in 511 the directory */ 512 ftp.setAsciiType(); 513 514 is = new FtpInputStream(ftp, ftp.list(null)); 515 msgh.add("content-type", "text/plain"); 516 msgh.add("access-type", "directory"); 517 } catch (IOException ex) { 518 throw new FileNotFoundException(fullpath); 519 } catch (FtpProtocolException ex2) { 520 throw new FileNotFoundException(fullpath); 521 } 522 } catch (FtpProtocolException ftpe) { 523 throw new IOException(ftpe); 524 } 525 setProperties(msgh); 526 return is; 527 } 528 529 /** 530 * Get the OutputStream to store the remote file. It will issue the 531 * "put" command to the ftp server. 532 * 533 * @return the <code>OutputStream</code> to the connection. 534 * 535 * @throws IOException if already opened for input or the URL 536 * points to a directory 537 * @throws FtpProtocolException if errors occur during the transfert. 538 */ 539 @Override getOutputStream()540 public OutputStream getOutputStream() throws IOException { 541 if (!connected) { 542 connect(); 543 } 544 545 // Android-changed: Removed support for proxying FTP over HTTP. 546 // It relies on the removed sun.net.www.protocol.http.HttpURLConnection API. 547 /* 548 if (http != null) { 549 OutputStream out = http.getOutputStream(); 550 // getInputStream() is neccessary to force a writeRequests() 551 // on the http client. 552 http.getInputStream(); 553 return out; 554 } 555 */ 556 557 if (is != null) { 558 throw new IOException("Already opened for input"); 559 } 560 561 if (os != null) { 562 return os; 563 } 564 565 decodePath(url.getPath()); 566 if (filename == null || filename.length() == 0) { 567 throw new IOException("illegal filename for a PUT"); 568 } 569 try { 570 if (pathname != null) { 571 cd(pathname); 572 } 573 if (type == ASCII) { 574 ftp.setAsciiType(); 575 } else { 576 ftp.setBinaryType(); 577 } 578 os = new FtpOutputStream(ftp, ftp.putFileStream(filename, false)); 579 } catch (FtpProtocolException e) { 580 throw new IOException(e); 581 } 582 return os; 583 } 584 guessContentTypeFromFilename(String fname)585 String guessContentTypeFromFilename(String fname) { 586 return guessContentTypeFromName(fname); 587 } 588 589 /** 590 * Gets the <code>Permission</code> associated with the host & port. 591 * 592 * @return The <code>Permission</code> object. 593 */ 594 @Override getPermission()595 public Permission getPermission() { 596 if (permission == null) { 597 int urlport = url.getPort(); 598 urlport = urlport < 0 ? FtpClient.defaultPort() : urlport; 599 String urlhost = this.host + ":" + urlport; 600 permission = new SocketPermission(urlhost, "connect"); 601 } 602 return permission; 603 } 604 605 /** 606 * Sets the general request property. If a property with the key already 607 * exists, overwrite its value with the new value. 608 * 609 * @param key the keyword by which the request is known 610 * (e.g., "<code>accept</code>"). 611 * @param value the value associated with it. 612 * @throws IllegalStateException if already connected 613 * @see #getRequestProperty(java.lang.String) 614 */ 615 @Override 616 public void setRequestProperty(String key, String value) { 617 super.setRequestProperty(key, value); 618 if ("type".equals(key)) { 619 if ("i".equalsIgnoreCase(value)) { 620 type = BIN; 621 } else if ("a".equalsIgnoreCase(value)) { 622 type = ASCII; 623 } else if ("d".equalsIgnoreCase(value)) { 624 type = DIR; 625 } else { 626 throw new IllegalArgumentException( 627 "Value of '" + key + 628 "' request property was '" + value + 629 "' when it must be either 'i', 'a' or 'd'"); 630 } 631 } 632 } 633 634 /** 635 * Returns the value of the named general request property for this 636 * connection. 637 * 638 * @param key the keyword by which the request is known (e.g., "accept"). 639 * @return the value of the named general request property for this 640 * connection. 641 * @throws IllegalStateException if already connected 642 * @see #setRequestProperty(java.lang.String, java.lang.String) 643 */ 644 @Override 645 public String getRequestProperty(String key) { 646 String value = super.getRequestProperty(key); 647 648 if (value == null) { 649 if ("type".equals(key)) { 650 value = (type == ASCII ? "a" : type == DIR ? "d" : "i"); 651 } 652 } 653 654 return value; 655 } 656 657 @Override 658 public void setConnectTimeout(int timeout) { 659 if (timeout < 0) { 660 throw new IllegalArgumentException("timeouts can't be negative"); 661 } 662 connectTimeout = timeout; 663 } 664 665 @Override 666 public int getConnectTimeout() { 667 return (connectTimeout < 0 ? 0 : connectTimeout); 668 } 669 670 @Override 671 public void setReadTimeout(int timeout) { 672 if (timeout < 0) { 673 throw new IllegalArgumentException("timeouts can't be negative"); 674 } 675 readTimeout = timeout; 676 } 677 678 @Override 679 public int getReadTimeout() { 680 return readTimeout < 0 ? 0 : readTimeout; 681 } 682 } 683