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