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