• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2006 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.webkit;
18 
19 import android.net.http.EventHandler;
20 import android.net.http.RequestHandle;
21 import android.util.Log;
22 import android.webkit.CacheManager.CacheResult;
23 
24 import java.util.HashMap;
25 import java.util.Map;
26 
27 class FrameLoader {
28 
29     private final LoadListener mListener;
30     private final String mMethod;
31     private final WebSettings mSettings;
32     private Map<String, String> mHeaders;
33     private byte[] mPostData;
34     private Network mNetwork;
35     private int mCacheMode;
36     private String mReferrer;
37     private String mContentType;
38 
39     private static final int URI_PROTOCOL = 0x100;
40 
41     private static final String CONTENT_TYPE = "content-type";
42 
43     // Contents of an about:blank page
44     private static final String mAboutBlank =
45             "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EB\">" +
46             "<html><head><title>about:blank</title></head><body></body></html>";
47 
48     static final String HEADER_STR = "text/xml, text/html, " +
49             "application/xhtml+xml, image/png, text/plain, */*;q=0.8";
50 
51     private static final String LOGTAG = "webkit";
52 
FrameLoader(LoadListener listener, WebSettings settings, String method)53     FrameLoader(LoadListener listener, WebSettings settings,
54             String method) {
55         mListener = listener;
56         mHeaders = null;
57         mMethod = method;
58         mCacheMode = WebSettings.LOAD_NORMAL;
59         mSettings = settings;
60     }
61 
setReferrer(String ref)62     public void setReferrer(String ref) {
63         // only set referrer for http or https
64         if (URLUtil.isNetworkUrl(ref)) mReferrer = ref;
65     }
66 
setPostData(byte[] postData)67     public void setPostData(byte[] postData) {
68         mPostData = postData;
69     }
70 
setContentTypeForPost(String postContentType)71     public void setContentTypeForPost(String postContentType) {
72         mContentType = postContentType;
73     }
74 
setCacheMode(int cacheMode)75     public void setCacheMode(int cacheMode) {
76         mCacheMode = cacheMode;
77     }
78 
setHeaders(HashMap headers)79     public void setHeaders(HashMap headers) {
80         mHeaders = headers;
81     }
82 
getLoadListener()83     public LoadListener getLoadListener() {
84         return mListener;
85     }
86 
87     /**
88      * Issues the load request.
89      *
90      * Return value does not indicate if the load was successful or not. It
91      * simply indicates that the load request is reasonable.
92      *
93      * @return true if the load is reasonable.
94      */
executeLoad()95     public boolean executeLoad() {
96         String url = mListener.url();
97 
98         if (URLUtil.isNetworkUrl(url)){
99             if (mSettings.getBlockNetworkLoads()) {
100                 mListener.error(EventHandler.ERROR_BAD_URL,
101                         mListener.getContext().getString(
102                                 com.android.internal.R.string.httpErrorBadUrl));
103                 return false;
104             }
105             // Make sure the host part of the url is correctly
106             // encoded before sending the request
107             if (!URLUtil.verifyURLEncoding(mListener.host())) {
108                 mListener.error(EventHandler.ERROR_BAD_URL,
109                         mListener.getContext().getString(
110                         com.android.internal.R.string.httpErrorBadUrl));
111                 return false;
112             }
113             mNetwork = Network.getInstance(mListener.getContext());
114             if (mListener.isSynchronous()) {
115                 return handleHTTPLoad();
116             }
117             WebViewWorker.getHandler().obtainMessage(
118                     WebViewWorker.MSG_ADD_HTTPLOADER, this).sendToTarget();
119             return true;
120         } else if (handleLocalFile(url, mListener, mSettings)) {
121             return true;
122         }
123         if (DebugFlags.FRAME_LOADER) {
124             Log.v(LOGTAG, "FrameLoader.executeLoad: url protocol not supported:"
125                     + mListener.url());
126         }
127         mListener.error(EventHandler.ERROR_UNSUPPORTED_SCHEME,
128                 mListener.getContext().getText(
129                         com.android.internal.R.string.httpErrorUnsupportedScheme).toString());
130         return false;
131 
132     }
133 
134     /* package */
handleLocalFile(String url, LoadListener loadListener, WebSettings settings)135     static boolean handleLocalFile(String url, LoadListener loadListener,
136             WebSettings settings) {
137         // Attempt to decode the percent-encoded url before passing to the
138         // local loaders.
139         try {
140             url = new String(URLUtil.decode(url.getBytes()));
141         } catch (IllegalArgumentException e) {
142             loadListener.error(EventHandler.ERROR_BAD_URL,
143                     loadListener.getContext().getString(
144                             com.android.internal.R.string.httpErrorBadUrl));
145             // Return true here so we do not trigger an unsupported scheme
146             // error.
147             return true;
148         }
149         if (URLUtil.isAssetUrl(url)) {
150             if (loadListener.isSynchronous()) {
151                 new FileLoader(url, loadListener, FileLoader.TYPE_ASSET,
152                         true).load();
153             } else {
154                 // load asset in a separate thread as it involves IO
155                 WebViewWorker.getHandler().obtainMessage(
156                         WebViewWorker.MSG_ADD_STREAMLOADER,
157                         new FileLoader(url, loadListener, FileLoader.TYPE_ASSET,
158                                 true)).sendToTarget();
159             }
160             return true;
161         } else if (URLUtil.isResourceUrl(url)) {
162             if (loadListener.isSynchronous()) {
163                 new FileLoader(url, loadListener, FileLoader.TYPE_RES,
164                         true).load();
165             } else {
166                 // load resource in a separate thread as it involves IO
167                 WebViewWorker.getHandler().obtainMessage(
168                         WebViewWorker.MSG_ADD_STREAMLOADER,
169                         new FileLoader(url, loadListener, FileLoader.TYPE_RES,
170                                 true)).sendToTarget();
171             }
172             return true;
173         } else if (URLUtil.isFileUrl(url)) {
174             if (loadListener.isSynchronous()) {
175                 new FileLoader(url, loadListener, FileLoader.TYPE_FILE,
176                         settings.getAllowFileAccess()).load();
177             } else {
178                 // load file in a separate thread as it involves IO
179                 WebViewWorker.getHandler().obtainMessage(
180                         WebViewWorker.MSG_ADD_STREAMLOADER,
181                         new FileLoader(url, loadListener, FileLoader.TYPE_FILE,
182                                 settings.getAllowFileAccess())).sendToTarget();
183             }
184             return true;
185         } else if (settings.getAllowContentAccess() &&
186                    URLUtil.isContentUrl(url)) {
187             // Send the raw url to the ContentLoader because it will do a
188             // permission check and the url has to match.
189             if (loadListener.isSynchronous()) {
190                 new ContentLoader(loadListener.url(), loadListener).load();
191             } else {
192                 // load content in a separate thread as it involves IO
193                 WebViewWorker.getHandler().obtainMessage(
194                         WebViewWorker.MSG_ADD_STREAMLOADER,
195                         new ContentLoader(loadListener.url(), loadListener))
196                         .sendToTarget();
197             }
198             return true;
199         } else if (URLUtil.isDataUrl(url)) {
200             // load data in the current thread to reduce the latency
201             new DataLoader(url, loadListener).load();
202             return true;
203         } else if (URLUtil.isAboutUrl(url)) {
204             loadListener.data(mAboutBlank.getBytes(), mAboutBlank.length());
205             loadListener.endData();
206             return true;
207         }
208         return false;
209     }
210 
handleHTTPLoad()211     boolean handleHTTPLoad() {
212         if (mHeaders == null) {
213             mHeaders = new HashMap<String, String>();
214         }
215         populateStaticHeaders();
216         populateHeaders();
217 
218         // response was handled by Cache, don't issue HTTP request
219         if (handleCache()) {
220             // push the request data down to the LoadListener
221             // as response from the cache could be a redirect
222             // and we may need to initiate a network request if the cache
223             // can't satisfy redirect URL
224             mListener.setRequestData(mMethod, mHeaders, mPostData);
225             return true;
226         }
227 
228         if (DebugFlags.FRAME_LOADER) {
229             Log.v(LOGTAG, "FrameLoader: http " + mMethod + " load for: "
230                     + mListener.url());
231         }
232 
233         boolean ret = false;
234         int error = EventHandler.ERROR_UNSUPPORTED_SCHEME;
235 
236         try {
237             ret = mNetwork.requestURL(mMethod, mHeaders,
238                     mPostData, mListener);
239         } catch (android.net.ParseException ex) {
240             error = EventHandler.ERROR_BAD_URL;
241         } catch (java.lang.RuntimeException ex) {
242             /* probably an empty header set by javascript.  We want
243                the same result as bad URL  */
244             error = EventHandler.ERROR_BAD_URL;
245         }
246         if (!ret) {
247             mListener.error(error, mListener.getContext().getText(
248                     EventHandler.errorStringResources[Math.abs(error)]).toString());
249             return false;
250         }
251         return true;
252     }
253 
254     /*
255      * This function is used by handleCache to
256      * setup a load from the byte stream in a CacheResult.
257      */
startCacheLoad(CacheResult result)258     private void startCacheLoad(CacheResult result) {
259         if (DebugFlags.FRAME_LOADER) {
260             Log.v(LOGTAG, "FrameLoader: loading from cache: "
261                   + mListener.url());
262         }
263         // Tell the Listener respond with the cache file
264         CacheLoader cacheLoader =
265                 new CacheLoader(mListener, result);
266         mListener.setCacheLoader(cacheLoader);
267         if (mListener.isSynchronous()) {
268             cacheLoader.load();
269         } else {
270             // Load the cached file in a separate thread
271             WebViewWorker.getHandler().obtainMessage(
272                     WebViewWorker.MSG_ADD_STREAMLOADER, cacheLoader).sendToTarget();
273         }
274     }
275 
276     /*
277      * This function is used by the handleHTTPLoad to setup the cache headers
278      * correctly.
279      * Returns true if the response was handled from the cache
280      */
handleCache()281     private boolean handleCache() {
282         switch (mCacheMode) {
283             // This mode is normally used for a reload, it instructs the http
284             // loader to not use the cached content.
285             case WebSettings.LOAD_NO_CACHE:
286                 break;
287 
288 
289             // This mode is used when the content should only be loaded from
290             // the cache. If it is not there, then fail the load. This is used
291             // to load POST content in a history navigation.
292             case WebSettings.LOAD_CACHE_ONLY: {
293                 CacheResult result = CacheManager.getCacheFile(mListener.url(),
294                         mListener.postIdentifier(), null);
295                 if (result != null) {
296                     startCacheLoad(result);
297                 } else {
298                     // This happens if WebCore was first told that the POST
299                     // response was in the cache, then when we try to use it
300                     // it has gone.
301                     // Generate a file not found error
302                     int err = EventHandler.FILE_NOT_FOUND_ERROR;
303                     mListener.error(err, mListener.getContext().getText(
304                             EventHandler.errorStringResources[Math.abs(err)])
305                             .toString());
306                 }
307                 return true;
308             }
309 
310             // This mode is for when the user is doing a history navigation
311             // in the browser and should returned cached content regardless
312             // of it's state. If it is not in the cache, then go to the
313             // network.
314             case WebSettings.LOAD_CACHE_ELSE_NETWORK: {
315                 if (DebugFlags.FRAME_LOADER) {
316                     Log.v(LOGTAG, "FrameLoader: checking cache: "
317                             + mListener.url());
318                 }
319                 // Get the cache file name for the current URL, passing null for
320                 // the validation headers causes no validation to occur
321                 CacheResult result = CacheManager.getCacheFile(mListener.url(),
322                         mListener.postIdentifier(), null);
323                 if (result != null) {
324                     startCacheLoad(result);
325                     return true;
326                 }
327                 break;
328             }
329 
330             // This is the default case, which is to check to see if the
331             // content in the cache can be used. If it can be used, then
332             // use it. If it needs revalidation then the relevant headers
333             // are added to the request.
334             default:
335             case WebSettings.LOAD_NORMAL:
336                 return mListener.checkCache(mHeaders);
337         }// end of switch
338 
339         return false;
340     }
341 
342     /**
343      * Add the static headers that don't change with each request.
344      */
populateStaticHeaders()345     private void populateStaticHeaders() {
346         // Accept header should already be there as they are built by WebCore,
347         // but in the case they are missing, add some.
348         String accept = mHeaders.get("Accept");
349         if (accept == null || accept.length() == 0) {
350             mHeaders.put("Accept", HEADER_STR);
351         }
352         mHeaders.put("Accept-Charset", "utf-8, iso-8859-1, utf-16, *;q=0.7");
353 
354         String acceptLanguage = mSettings.getAcceptLanguage();
355         if (acceptLanguage.length() > 0) {
356             mHeaders.put("Accept-Language", acceptLanguage);
357         }
358 
359         mHeaders.put("User-Agent", mSettings.getUserAgentString());
360     }
361 
362     /**
363      * Add the content related headers. These headers contain user private data
364      * and is not used when we are proxying an untrusted request.
365      */
populateHeaders()366     private void populateHeaders() {
367 
368         if (mReferrer != null) mHeaders.put("Referer", mReferrer);
369         if (mContentType != null) mHeaders.put(CONTENT_TYPE, mContentType);
370 
371         // if we have an active proxy and have proxy credentials, do pre-emptive
372         // authentication to avoid an extra round-trip:
373         if (mNetwork.isValidProxySet()) {
374             String username;
375             String password;
376             /* The proxy credentials can be set in the Network thread */
377             synchronized (mNetwork) {
378                 username = mNetwork.getProxyUsername();
379                 password = mNetwork.getProxyPassword();
380             }
381             if (username != null && password != null) {
382                 // we collect credentials ONLY if the proxy scheme is BASIC!!!
383                 String proxyHeader = RequestHandle.authorizationHeader(true);
384                 mHeaders.put(proxyHeader,
385                         "Basic " + RequestHandle.computeBasicAuthResponse(
386                                 username, password));
387             }
388         }
389 
390         // Set cookie header
391         String cookie = CookieManager.getInstance().getCookie(
392                 mListener.getWebAddress());
393         if (cookie != null && cookie.length() > 0) {
394             mHeaders.put("Cookie", cookie);
395         }
396     }
397 }
398