1 /* 2 * Copyright (c) 2011, 2018, 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 package com.sun.webkit.network; 27 28 import com.sun.javafx.logging.PlatformLogger; 29 import com.sun.javafx.logging.PlatformLogger.Level; 30 import com.sun.webkit.Invoker; 31 import com.sun.webkit.LoadListenerClient; 32 import com.sun.webkit.WebPage; 33 import static com.sun.webkit.network.URLs.newURL; 34 import java.io.EOFException; 35 import java.io.File; 36 import java.io.FileNotFoundException; 37 import java.io.IOException; 38 import java.io.InputStream; 39 import java.io.OutputStream; 40 import java.io.UnsupportedEncodingException; 41 import java.lang.annotation.Native; 42 import java.net.ConnectException; 43 import java.net.HttpRetryException; 44 import java.net.HttpURLConnection; 45 import java.net.MalformedURLException; 46 import java.net.NoRouteToHostException; 47 import java.net.SocketException; 48 import java.net.SocketTimeoutException; 49 import java.net.URL; 50 import java.net.URLConnection; 51 import java.net.URLDecoder; 52 import java.net.UnknownHostException; 53 import java.nio.ByteBuffer; 54 import java.security.AccessControlException; 55 import java.security.AccessController; 56 import java.security.PrivilegedAction; 57 import java.util.List; 58 import java.util.Locale; 59 import java.util.Map; 60 import java.util.concurrent.CountDownLatch; 61 import java.util.zip.GZIPInputStream; 62 import java.util.zip.InflaterInputStream; 63 import javax.net.ssl.SSLHandshakeException; 64 65 /** 66 * A runnable that loads a resource specified by a URL. 67 */ 68 final class URLLoader implements Runnable { 69 70 @Native public static final int ALLOW_UNASSIGNED = java.net.IDN.ALLOW_UNASSIGNED; 71 72 private static final PlatformLogger logger = 73 PlatformLogger.getLogger(URLLoader.class.getName()); 74 private static final int MAX_REDIRECTS = 10; 75 private static final int MAX_BUF_COUNT = 3; 76 private static final String GET = "GET"; 77 private static final String HEAD = "HEAD"; 78 private static final String DELETE = "DELETE"; 79 80 81 private final WebPage webPage; 82 private final ByteBufferPool byteBufferPool; 83 private final boolean asynchronous; 84 private String url; 85 private String method; 86 private final String headers; 87 private FormDataElement[] formDataElements; 88 private final long data; 89 private volatile boolean canceled = false; 90 91 92 /** 93 * Creates a new {@code URLLoader}. 94 */ 95 URLLoader(WebPage webPage, 96 ByteBufferPool byteBufferPool, 97 boolean asynchronous, 98 String url, 99 String method, 100 String headers, 101 FormDataElement[] formDataElements, 102 long data) 103 { 104 this.webPage = webPage; 105 this.byteBufferPool = byteBufferPool; 106 this.asynchronous = asynchronous; 107 this.url = url; 108 this.method = method; 109 this.headers = headers; 110 this.formDataElements = formDataElements; 111 this.data = data; 112 } 113 114 115 /** 116 * Cancels this loader. 117 */ 118 private void fwkCancel() { 119 if (logger.isLoggable(Level.FINEST)) { 120 logger.finest(String.format("data: [0x%016X]", data)); 121 } 122 canceled = true; 123 } 124 125 /** 126 * {@inheritDoc} 127 */ 128 @Override 129 public void run() { 130 // Run the loader in the page's access control context 131 AccessController.doPrivileged((PrivilegedAction<Void>) () -> { 132 doRun(); 133 return null; 134 }, webPage.getAccessControlContext()); 135 } 136 137 /** 138 * Executes this loader. 139 */ 140 private void doRun() { 141 Throwable error = null; 142 int errorCode = 0; 143 try { 144 int redirectCount = 0; 145 boolean streaming = true; 146 boolean connectionResetRetry = true; 147 while (true) { 148 // RT-14438 149 String actualUrl = url; 150 if (url.startsWith("file:")) { 151 int questionMarkPosition = url.indexOf('?'); 152 if (questionMarkPosition != -1) { 153 actualUrl = url.substring(0, questionMarkPosition); 154 } 155 } 156 157 URL urlObject = newURL(actualUrl); 158 159 // RT-22458 160 workaround7177996(urlObject); 161 162 URLConnection c = urlObject.openConnection(); 163 prepareConnection(c); 164 165 Redirect redirect = null; 166 try { 167 sendRequest(c, streaming); 168 redirect = receiveResponse(c); 169 } catch (HttpRetryException ex) { 170 // RT-19914 171 if (streaming) { 172 streaming = false; 173 continue; // retry without streaming 174 } else { 175 throw ex; 176 } 177 } catch (SocketException ex) { 178 // SocketException: Connection reset, Retry once 179 if ("Connection reset".equals(ex.getMessage()) && connectionResetRetry) { 180 connectionResetRetry = false; 181 continue; 182 } else { 183 throw ex; 184 } 185 } finally { 186 close(c); 187 } 188 189 if (redirect != null) { 190 if (redirectCount++ >= MAX_REDIRECTS) { 191 throw new TooManyRedirectsException(); 192 } 193 boolean resetRequest = !redirect.preserveRequest 194 && !method.equals(GET) && !method.equals(HEAD); 195 String newMethod = resetRequest ? GET : method; 196 willSendRequest(redirect.url, newMethod, c); 197 // willSendRequest() may cancel this loader 198 if (canceled) { 199 break; 200 } 201 url = redirect.url; 202 method = newMethod; 203 formDataElements = resetRequest ? null : formDataElements; 204 } else { 205 break; 206 } 207 } 208 } catch (MalformedURLException ex) { 209 error = ex; 210 errorCode = LoadListenerClient.MALFORMED_URL; 211 } catch (AccessControlException ex) { 212 error = ex; 213 errorCode = LoadListenerClient.PERMISSION_DENIED; 214 } catch (UnknownHostException ex) { 215 error = ex; 216 errorCode = LoadListenerClient.UNKNOWN_HOST; 217 } catch (NoRouteToHostException ex) { 218 error = ex; 219 errorCode = LoadListenerClient.NO_ROUTE_TO_HOST; 220 } catch (ConnectException ex) { 221 error = ex; 222 errorCode = LoadListenerClient.CONNECTION_REFUSED; 223 } catch (SocketException ex) { 224 error = ex; 225 errorCode = LoadListenerClient.CONNECTION_RESET; 226 } catch (SSLHandshakeException ex) { 227 error = ex; 228 errorCode = LoadListenerClient.SSL_HANDSHAKE; 229 } catch (SocketTimeoutException ex) { 230 error = ex; 231 errorCode = LoadListenerClient.CONNECTION_TIMED_OUT; 232 } catch (InvalidResponseException ex) { 233 error = ex; 234 errorCode = LoadListenerClient.INVALID_RESPONSE; 235 } catch (TooManyRedirectsException ex) { 236 error = ex; 237 errorCode = LoadListenerClient.TOO_MANY_REDIRECTS; 238 } catch (FileNotFoundException ex) { 239 error = ex; 240 errorCode = LoadListenerClient.FILE_NOT_FOUND; 241 } catch (Throwable th) { 242 error = th; 243 errorCode = LoadListenerClient.UNKNOWN_ERROR; 244 } 245 246 if (error != null) { 247 if (errorCode == LoadListenerClient.UNKNOWN_ERROR) { 248 logger.warning("Unexpected error", error); 249 } else { 250 logger.finest("Load error", error); 251 } 252 didFail(errorCode, error.getMessage()); 253 } 254 } 255 256 private static void workaround7177996(URL url) 257 throws FileNotFoundException 258 { 259 if (!url.getProtocol().equals("file")) { 260 return; 261 } 262 263 String host = url.getHost(); 264 if (host == null || host.equals("") || host.equals("~") 265 || host.equalsIgnoreCase("localhost") ) 266 { 267 return; 268 } 269 270 if (System.getProperty("os.name").startsWith("Windows")) { 271 String path = null; 272 try { 273 path = URLDecoder.decode(url.getPath(), "UTF-8"); 274 } catch (UnsupportedEncodingException e) { 275 // The system should always have the platform default 276 } 277 path = path.replace('/', '\\'); 278 path = path.replace('|', ':'); 279 File file = new File("\\\\" + host + path); 280 if (!file.exists()) { 281 throw new FileNotFoundException("File not found: " + url); 282 } 283 } else { 284 throw new FileNotFoundException("File not found: " + url); 285 } 286 } 287 288 /** 289 * Prepares a connection. 290 */ 291 private void prepareConnection(URLConnection c) throws IOException { 292 // The following two timeouts are quite arbitrary and should 293 // probably be configurable via an API 294 c.setConnectTimeout(30000); // 30 seconds 295 c.setReadTimeout(60000 * 60); // 60 minutes 296 297 // Given that WebKit has its own cache, do not use 298 // any URLConnection caches, even if someone installs them. 299 // As a side effect, this fixes the problem of WebPane not 300 // working well with the plug-in cache, which was one of 301 // the causes for RT-11880. 302 c.setUseCaches(false); 303 304 Locale loc = Locale.getDefault(); 305 String lang = ""; 306 if (!loc.equals(Locale.US) && !loc.equals(Locale.ENGLISH)) { 307 lang = loc.getCountry().isEmpty() ? 308 loc.getLanguage() + ",": 309 loc.getLanguage() + "-" + loc.getCountry() + ","; 310 } 311 c.setRequestProperty("Accept-Language", lang.toLowerCase() + "en-us;q=0.8,en;q=0.7"); 312 c.setRequestProperty("Accept-Encoding", "gzip"); 313 c.setRequestProperty("Accept-Charset", "ISO-8859-1,utf-8;q=0.7,*;q=0.7"); 314 315 if (headers != null && headers.length() > 0) { 316 for (String h : headers.split("\n")) { 317 int i = h.indexOf(':'); 318 if (i > 0) { 319 c.addRequestProperty(h.substring(0, i), h.substring(i + 2)); 320 } 321 } 322 } 323 324 if (c instanceof HttpURLConnection) { 325 HttpURLConnection httpConnection = (HttpURLConnection) c; 326 httpConnection.setRequestMethod(method); 327 // There are too many bugs in the way HttpURLConnection handles 328 // redirects, so we will deal with them ourselves 329 httpConnection.setInstanceFollowRedirects(false); 330 } 331 } 332 333 /** 334 * Sends request to the server. 335 */ 336 private void sendRequest(URLConnection c, boolean streaming) 337 throws IOException 338 { 339 OutputStream out = null; 340 try { 341 long bytesToBeSent = 0; 342 boolean sendFormData = formDataElements != null 343 && c instanceof HttpURLConnection 344 && !method.equals(DELETE); 345 boolean isGetOrHead = method.equals(GET) || method.equals(HEAD); 346 if (sendFormData) { 347 c.setDoOutput(true); 348 349 for (FormDataElement formDataElement : formDataElements) { 350 formDataElement.open(); 351 bytesToBeSent += formDataElement.getSize(); 352 } 353 354 if (streaming) { 355 HttpURLConnection http = (HttpURLConnection) c; 356 if (bytesToBeSent <= Integer.MAX_VALUE) { 357 http.setFixedLengthStreamingMode((int) bytesToBeSent); 358 } else { 359 http.setChunkedStreamingMode(0); 360 } 361 } 362 } else if (!isGetOrHead && (c instanceof HttpURLConnection)) { 363 c.setRequestProperty("Content-Length", "0"); 364 } 365 366 int maxTryCount = isGetOrHead ? 3 : 1; 367 c.setConnectTimeout(c.getConnectTimeout() / maxTryCount); 368 int tryCount = 0; 369 while (!canceled) { 370 try { 371 c.connect(); 372 break; 373 } catch (SocketTimeoutException ex) { 374 if (++tryCount >= maxTryCount) { 375 throw ex; 376 } 377 } catch (IllegalArgumentException ex) { 378 // Happens with some malformed URLs 379 throw new MalformedURLException(url); 380 } 381 } 382 383 if (sendFormData) { 384 out = c.getOutputStream(); 385 byte[] buffer = new byte[4096]; 386 long bytesSent = 0; 387 for (FormDataElement formDataElement : formDataElements) { 388 InputStream in = formDataElement.getInputStream(); 389 int count; 390 while ((count = in.read(buffer)) > 0) { 391 out.write(buffer, 0, count); 392 bytesSent += count; 393 didSendData(bytesSent, bytesToBeSent); 394 } 395 formDataElement.close(); 396 } 397 out.flush(); 398 out.close(); 399 out = null; 400 } 401 } finally { 402 if (out != null) { 403 try { 404 out.close(); 405 } catch (IOException ignore) {} 406 } 407 if (formDataElements != null && c instanceof HttpURLConnection) { 408 for (FormDataElement formDataElement : formDataElements) { 409 try { 410 formDataElement.close(); 411 } catch (IOException ignore) {} 412 } 413 } 414 } 415 } 416 417 /** 418 * Receives response from the server. 419 */ 420 private Redirect receiveResponse(URLConnection c) 421 throws IOException, InterruptedException 422 { 423 if (canceled) { 424 return null; 425 } 426 427 InputStream errorStream = null; 428 429 if (c instanceof HttpURLConnection) { 430 HttpURLConnection http = (HttpURLConnection) c; 431 432 int code = http.getResponseCode(); 433 if (code == -1) { 434 throw new InvalidResponseException(); 435 } 436 437 if (canceled) { 438 return null; 439 } 440 441 // See RT-17435 442 switch (code) { 443 case 301: // Moved Permanently 444 case 302: // Found 445 case 303: // See Other 446 case 307: // Temporary Redirect 447 String newLoc = http.getHeaderField("Location"); 448 if (newLoc != null) { 449 URL newUrl; 450 try { 451 newUrl = newURL(newLoc); 452 } catch (MalformedURLException mue) { 453 // Try to treat newLoc as a relative URI to conform 454 // to popular browsers 455 newUrl = newURL(c.getURL(), newLoc); 456 } 457 return new Redirect(newUrl.toExternalForm(), 458 code == 307); 459 } 460 break; 461 462 case 304: // Not Modified 463 didReceiveResponse(c); 464 didFinishLoading(); 465 return null; 466 } 467 468 if (code >= 400 && !method.equals(HEAD)) { 469 errorStream = http.getErrorStream(); 470 } 471 } 472 473 // Let's see if it's an ftp (or ftps) URL and we need to transform 474 // a directory listing into HTML 475 if (url.startsWith("ftp:") || url.startsWith("ftps:")) { 476 boolean dir = false; 477 boolean notsure = false; 478 // Unfortunately, there is no clear way to determine if we are 479 // accessing a directory, so a bit of guessing is in order 480 String path = c.getURL().getPath(); 481 if (path == null || path.isEmpty() || path.endsWith("/") 482 || path.contains(";type=d")) 483 { 484 dir = true; 485 } else { 486 String type = c.getContentType(); 487 if ("text/plain".equalsIgnoreCase(type) 488 || "text/html".equalsIgnoreCase(type)) 489 { 490 dir = true; 491 notsure = true; 492 } 493 } 494 if (dir) { 495 c = new DirectoryURLConnection(c, notsure); 496 } 497 } 498 499 // Same is true for FileURLConnection 500 if (url.startsWith("file:")) { 501 if("text/plain".equals(c.getContentType()) 502 && c.getHeaderField("content-length") == null) 503 { 504 // It is a directory 505 c = new DirectoryURLConnection(c); 506 } 507 } 508 509 didReceiveResponse(c); 510 511 if (method.equals(HEAD)) { 512 didFinishLoading(); 513 return null; 514 } 515 516 InputStream inputStream = null; 517 try { 518 inputStream = errorStream == null 519 ? c.getInputStream() : errorStream; 520 } catch (HttpRetryException ex) { 521 // HttpRetryException is handled from doRun() method. 522 // Hence rethrowing the exception to caller(doRun() method) 523 throw ex; 524 } catch (IOException e) { 525 if (logger.isLoggable(Level.FINE)) { 526 logger.fine(String.format("Exception caught: [%s], %s", 527 e.getClass().getSimpleName(), 528 e.getMessage())); 529 } 530 } 531 532 String encoding = c.getContentEncoding(); 533 if (inputStream != null) { 534 try { 535 if ("gzip".equalsIgnoreCase(encoding)) { 536 inputStream = new GZIPInputStream(inputStream); 537 } else if ("deflate".equalsIgnoreCase(encoding)) { 538 inputStream = new InflaterInputStream(inputStream); 539 } 540 } catch (IOException e) { 541 if (logger.isLoggable(Level.FINE)) { 542 logger.fine(String.format("Exception caught: [%s], %s", 543 e.getClass().getSimpleName(), 544 e.getMessage())); 545 } 546 } 547 } 548 549 ByteBufferAllocator allocator = 550 byteBufferPool.newAllocator(MAX_BUF_COUNT); 551 ByteBuffer byteBuffer = null; 552 try { 553 if (inputStream != null) { 554 // 8192 is the default size of a BufferedInputStream used in 555 // most URLConnections, by using the same size, we avoid quite 556 // a few System.arrayCopy() calls 557 byte[] buffer = new byte[8192]; 558 while (!canceled) { 559 int count; 560 try { 561 count = inputStream.read(buffer); 562 } catch (EOFException ex) { 563 // can be thrown by GZIPInputStream signaling 564 // the end of the stream 565 count = -1; 566 } 567 568 if (count == -1) { 569 break; 570 } 571 572 if (byteBuffer == null) { 573 byteBuffer = allocator.allocate(); 574 } 575 576 int remaining = byteBuffer.remaining(); 577 if (count < remaining) { 578 byteBuffer.put(buffer, 0, count); 579 } else { 580 byteBuffer.put(buffer, 0, remaining); 581 582 byteBuffer.flip(); 583 didReceiveData(byteBuffer, allocator); 584 byteBuffer = null; 585 586 int outstanding = count - remaining; 587 if (outstanding > 0) { 588 byteBuffer = allocator.allocate(); 589 byteBuffer.put(buffer, remaining, outstanding); 590 } 591 } 592 } 593 } 594 if (!canceled) { 595 if (byteBuffer != null && byteBuffer.position() > 0) { 596 byteBuffer.flip(); 597 didReceiveData(byteBuffer, allocator); 598 byteBuffer = null; 599 } 600 didFinishLoading(); 601 } 602 } finally { 603 if (byteBuffer != null) { 604 byteBuffer.clear(); 605 allocator.release(byteBuffer); 606 } 607 } 608 return null; 609 } 610 611 /** 612 * Releases the resources that may be associated with a connection. 613 */ 614 private static void close(URLConnection c) { 615 if (c instanceof HttpURLConnection) { 616 InputStream errorStream = ((HttpURLConnection) c).getErrorStream(); 617 if (errorStream != null) { 618 try { 619 errorStream.close(); 620 } catch (IOException ignore) {} 621 } 622 } 623 try { 624 c.getInputStream().close(); 625 } catch (IOException ignore) {} 626 } 627 628 629 /** 630 * A holder for redirect information. 631 */ 632 private static final class Redirect { 633 private final String url; 634 private final boolean preserveRequest; 635 636 private Redirect(String url, boolean preserveRequest) { 637 this.url = url; 638 this.preserveRequest = preserveRequest; 639 } 640 } 641 642 /** 643 * Signals an invalid response from the server. 644 */ 645 private static final class InvalidResponseException extends IOException { 646 private InvalidResponseException() { 647 super("Invalid server response"); 648 } 649 } 650 651 /** 652 * Signals that too many redirects have been encountered 653 * while processing the request. 654 */ 655 private static final class TooManyRedirectsException extends IOException { 656 private TooManyRedirectsException() { 657 super("Too many redirects"); 658 } 659 } 660 661 private void didSendData(final long totalBytesSent, 662 final long totalBytesToBeSent) 663 { 664 callBack(() -> { 665 if (!canceled) { 666 notifyDidSendData(totalBytesSent, totalBytesToBeSent); 667 } 668 }); 669 } 670 671 private void notifyDidSendData(long totalBytesSent, 672 long totalBytesToBeSent) 673 { 674 if (logger.isLoggable(Level.FINEST)) { 675 logger.finest(String.format( 676 "totalBytesSent: [%d], " 677 + "totalBytesToBeSent: [%d], " 678 + "data: [0x%016X]", 679 totalBytesSent, 680 totalBytesToBeSent, 681 data)); 682 } 683 twkDidSendData(totalBytesSent, totalBytesToBeSent, data); 684 } 685 686 private void willSendRequest(String newUrl, 687 final String newMethod, 688 URLConnection c) throws InterruptedException 689 { 690 final String adjustedNewUrl = adjustUrlForWebKit(newUrl); 691 final int status = extractStatus(c); 692 final String contentType = c.getContentType(); 693 final String contentEncoding = extractContentEncoding(c); 694 final long contentLength = extractContentLength(c); 695 final String responseHeaders = extractHeaders(c); 696 final String adjustedUrl = adjustUrlForWebKit(url); 697 final CountDownLatch latch = 698 asynchronous ? new CountDownLatch(1) : null; 699 callBack(() -> { 700 try { 701 if (!canceled) { 702 boolean keepGoing = notifyWillSendRequest( 703 adjustedNewUrl, 704 newMethod, 705 status, 706 contentType, 707 contentEncoding, 708 contentLength, 709 responseHeaders, 710 adjustedUrl); 711 if (!keepGoing) { 712 fwkCancel(); 713 } 714 } 715 } finally { 716 if (latch != null) { 717 latch.countDown(); 718 } 719 } 720 }); 721 if (latch != null) { 722 latch.await(); 723 } 724 } 725 726 private boolean notifyWillSendRequest(String newUrl, 727 String newMethod, 728 int status, 729 String contentType, 730 String contentEncoding, 731 long contentLength, 732 String headers, 733 String url) 734 { 735 if (logger.isLoggable(Level.FINEST)) { 736 logger.finest(String.format( 737 "newUrl: [%s], " 738 + "newMethod: [%s], " 739 + "status: [%d], " 740 + "contentType: [%s], " 741 + "contentEncoding: [%s], " 742 + "contentLength: [%d], " 743 + "url: [%s], " 744 + "data: [0x%016X], " 745 + "headers:%n%s", 746 newUrl, 747 newMethod, 748 status, 749 contentType, 750 contentEncoding, 751 contentLength, 752 url, 753 data, 754 Util.formatHeaders(headers))); 755 } 756 boolean result = twkWillSendRequest( 757 newUrl, 758 newMethod, 759 status, 760 contentType, 761 contentEncoding, 762 contentLength, 763 headers, 764 url, 765 data); 766 if (logger.isLoggable(Level.FINEST)) { 767 logger.finest(String.format("result: [%s]", result)); 768 } 769 return result; 770 } 771 772 private void didReceiveResponse(URLConnection c) { 773 final int status = extractStatus(c); 774 final String contentType = c.getContentType(); 775 final String contentEncoding = extractContentEncoding(c); 776 final long contentLength = extractContentLength(c); 777 final String responseHeaders = extractHeaders(c); 778 final String adjustedUrl = adjustUrlForWebKit(url); 779 callBack(() -> { 780 if (!canceled) { 781 notifyDidReceiveResponse( 782 status, 783 contentType, 784 contentEncoding, 785 contentLength, 786 responseHeaders, 787 adjustedUrl); 788 } 789 }); 790 } 791 792 private void notifyDidReceiveResponse(int status, 793 String contentType, 794 String contentEncoding, 795 long contentLength, 796 String headers, 797 String url) 798 { 799 if (logger.isLoggable(Level.FINEST)) { 800 logger.finest(String.format( 801 "status: [%d], " 802 + "contentType: [%s], " 803 + "contentEncoding: [%s], " 804 + "contentLength: [%d], " 805 + "url: [%s], " 806 + "data: [0x%016X], " 807 + "headers:%n%s", 808 status, 809 contentType, 810 contentEncoding, 811 contentLength, 812 url, 813 data, 814 Util.formatHeaders(headers))); 815 } 816 twkDidReceiveResponse( 817 status, 818 contentType, 819 contentEncoding, 820 contentLength, 821 headers, 822 url, 823 data); 824 } 825 826 private void didReceiveData(final ByteBuffer byteBuffer, 827 final ByteBufferAllocator allocator) 828 { 829 callBack(() -> { 830 if (!canceled) { 831 notifyDidReceiveData( 832 byteBuffer, 833 byteBuffer.position(), 834 byteBuffer.remaining()); 835 } 836 byteBuffer.clear(); 837 allocator.release(byteBuffer); 838 }); 839 } 840 841 private void notifyDidReceiveData(ByteBuffer byteBuffer, 842 int position, 843 int remaining) 844 { 845 if (logger.isLoggable(Level.FINEST)) { 846 logger.finest(String.format( 847 "byteBuffer: [%s], " 848 + "position: [%s], " 849 + "remaining: [%s], " 850 + "data: [0x%016X]", 851 byteBuffer, 852 position, 853 remaining, 854 data)); 855 } 856 twkDidReceiveData(byteBuffer, position, remaining, data); 857 } 858 859 private void didFinishLoading() { 860 callBack(() -> { 861 if (!canceled) { 862 notifyDidFinishLoading(); 863 } 864 }); 865 } 866 867 private void notifyDidFinishLoading() { 868 if (logger.isLoggable(Level.FINEST)) { 869 logger.finest(String.format("data: [0x%016X]", data)); 870 } 871 twkDidFinishLoading(data); 872 } 873 874 private void didFail(final int errorCode, final String message) { 875 final String adjustedUrl = adjustUrlForWebKit(url); 876 callBack(() -> { 877 if (!canceled) { 878 notifyDidFail(errorCode, adjustedUrl, message); 879 } 880 }); 881 } 882 883 private void notifyDidFail(int errorCode, String url, String message) { 884 if (logger.isLoggable(Level.FINEST)) { 885 logger.finest(String.format( 886 "errorCode: [%d], " 887 + "url: [%s], " 888 + "message: [%s], " 889 + "data: [0x%016X]", 890 errorCode, 891 url, 892 message, 893 data)); 894 } 895 twkDidFail(errorCode, url, message, data); 896 } 897 898 private void callBack(Runnable runnable) { 899 if (asynchronous) { 900 Invoker.getInvoker().invokeOnEventThread(runnable); 901 } else { 902 runnable.run(); 903 } 904 } 905 906 private static native void twkDidSendData(long totalBytesSent, 907 long totalBytesToBeSent, 908 long data); 909 910 private static native boolean twkWillSendRequest(String newUrl, 911 String newMethod, 912 int status, 913 String contentType, 914 String contentEncoding, 915 long contentLength, 916 String headers, 917 String url, 918 long data); 919 920 private static native void twkDidReceiveResponse(int status, 921 String contentType, 922 String contentEncoding, 923 long contentLength, 924 String headers, 925 String url, 926 long data); 927 928 private static native void twkDidReceiveData(ByteBuffer byteBuffer, 929 int position, 930 int remaining, 931 long data); 932 933 private static native void twkDidFinishLoading(long data); 934 935 private static native void twkDidFail(int errorCode, 936 String url, 937 String message, 938 long data); 939 940 /** 941 * Given a {@link URLConnection}, returns the connection status 942 * for passing into native callbacks. 943 */ 944 private static int extractStatus(URLConnection c) { 945 int status = 0; 946 if (c instanceof HttpURLConnection) { 947 try { 948 status = ((HttpURLConnection) c).getResponseCode(); 949 } catch (java.io.IOException ignore) {} 950 } 951 return status; 952 } 953 954 /** 955 * Given a {@link URLConnection}, returns the content encoding 956 * for passing into native callbacks. 957 */ 958 private static String extractContentEncoding(URLConnection c) { 959 String contentEncoding = c.getContentEncoding(); 960 // For compressed streams, the encoding is in Content-Type 961 if ("gzip".equalsIgnoreCase(contentEncoding) || 962 "deflate".equalsIgnoreCase(contentEncoding)) 963 { 964 contentEncoding = null; 965 String contentType = c.getContentType(); 966 if (contentType != null) { 967 int i = contentType.indexOf("charset="); 968 if (i >= 0) { 969 contentEncoding = contentType.substring(i + 8); 970 i = contentEncoding.indexOf(";"); 971 if (i > 0) { 972 contentEncoding = contentEncoding.substring(0, i); 973 } 974 } 975 } 976 } 977 return contentEncoding; 978 } 979 980 /** 981 * Given a {@link URLConnection}, returns the content length 982 * for passing into native callbacks. 983 */ 984 private static long extractContentLength(URLConnection c) { 985 // Cannot use URLConnection.getContentLength() 986 // as it only returns an int 987 try { 988 return Long.parseLong(c.getHeaderField("content-length")); 989 } catch (Exception ex) { 990 return -1; 991 } 992 } 993 994 /** 995 * Given a {@link URLConnection}, returns the headers string 996 * for passing into native callbacks. 997 */ 998 private static String extractHeaders(URLConnection c) { 999 StringBuilder sb = new StringBuilder(); 1000 Map<String, List<String>> headers = c.getHeaderFields(); 1001 for (Map.Entry<String, List<String>> entry: headers.entrySet()) { 1002 String key = entry.getKey(); 1003 List<String> values = entry.getValue(); 1004 for (String value : values) { 1005 sb.append(key != null ? key : ""); 1006 sb.append(':').append(value).append('\n'); 1007 } 1008 } 1009 return sb.toString(); 1010 } 1011 1012 /** 1013 * Adjust a URL string for passing into WebKit. 1014 */ 1015 private static String adjustUrlForWebKit(String url) { 1016 try { 1017 url = Util.adjustUrlForWebKit(url); 1018 } catch (Exception ignore) { 1019 } 1020 return url; 1021 } 1022 }