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 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 62 public void setReferrer(String ref) { 63 // only set referrer for http or https 64 if (URLUtil.isNetworkUrl(ref)) mReferrer = ref; 65 } 66 67 public void setPostData(byte[] postData) { 68 mPostData = postData; 69 } 70 71 public void setContentTypeForPost(String postContentType) { 72 mContentType = postContentType; 73 } 74 75 public void setCacheMode(int cacheMode) { 76 mCacheMode = cacheMode; 77 } 78 79 public void setHeaders(HashMap headers) { 80 mHeaders = headers; 81 } 82 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 */ 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 */ 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 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 */ 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 */ 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 */ 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 */ 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