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 }