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