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