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