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