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 it is correctly URL encoded before sending the request 106 if (!URLUtil.verifyURLEncoding(url)) { 107 mListener.error(EventHandler.ERROR_BAD_URL, 108 mListener.getContext().getString( 109 com.android.internal.R.string.httpErrorBadUrl)); 110 return false; 111 } 112 mNetwork = Network.getInstance(mListener.getContext()); 113 return handleHTTPLoad(); 114 } else if (handleLocalFile(url, mListener, mSettings)) { 115 return true; 116 } 117 if (DebugFlags.FRAME_LOADER) { 118 Log.v(LOGTAG, "FrameLoader.executeLoad: url protocol not supported:" 119 + mListener.url()); 120 } 121 mListener.error(EventHandler.ERROR_UNSUPPORTED_SCHEME, 122 mListener.getContext().getText( 123 com.android.internal.R.string.httpErrorUnsupportedScheme).toString()); 124 return false; 125 126 } 127 128 /* package */ handleLocalFile(String url, LoadListener loadListener, WebSettings settings)129 static boolean handleLocalFile(String url, LoadListener loadListener, 130 WebSettings settings) { 131 // Attempt to decode the percent-encoded url before passing to the 132 // local loaders. 133 try { 134 url = new String(URLUtil.decode(url.getBytes())); 135 } catch (IllegalArgumentException e) { 136 loadListener.error(EventHandler.ERROR_BAD_URL, 137 loadListener.getContext().getString( 138 com.android.internal.R.string.httpErrorBadUrl)); 139 // Return true here so we do not trigger an unsupported scheme 140 // error. 141 return true; 142 } 143 if (URLUtil.isAssetUrl(url)) { 144 FileLoader.requestUrl(url, loadListener, loadListener.getContext(), 145 true, settings.getAllowFileAccess()); 146 return true; 147 } else if (URLUtil.isFileUrl(url)) { 148 FileLoader.requestUrl(url, loadListener, loadListener.getContext(), 149 false, settings.getAllowFileAccess()); 150 return true; 151 } else if (URLUtil.isContentUrl(url)) { 152 // Send the raw url to the ContentLoader because it will do a 153 // permission check and the url has to match.. 154 ContentLoader.requestUrl(loadListener.url(), loadListener, 155 loadListener.getContext()); 156 return true; 157 } else if (URLUtil.isDataUrl(url)) { 158 DataLoader.requestUrl(url, loadListener); 159 return true; 160 } else if (URLUtil.isAboutUrl(url)) { 161 loadListener.data(mAboutBlank.getBytes(), mAboutBlank.length()); 162 loadListener.endData(); 163 return true; 164 } 165 return false; 166 } 167 handleHTTPLoad()168 private boolean handleHTTPLoad() { 169 if (mHeaders == null) { 170 mHeaders = new HashMap<String, String>(); 171 } 172 populateStaticHeaders(); 173 populateHeaders(); 174 175 // response was handled by Cache, don't issue HTTP request 176 if (handleCache()) { 177 // push the request data down to the LoadListener 178 // as response from the cache could be a redirect 179 // and we may need to initiate a network request if the cache 180 // can't satisfy redirect URL 181 mListener.setRequestData(mMethod, mHeaders, mPostData); 182 return true; 183 } 184 185 if (DebugFlags.FRAME_LOADER) { 186 Log.v(LOGTAG, "FrameLoader: http " + mMethod + " load for: " 187 + mListener.url()); 188 } 189 190 boolean ret = false; 191 int error = EventHandler.ERROR_UNSUPPORTED_SCHEME; 192 193 try { 194 ret = mNetwork.requestURL(mMethod, mHeaders, 195 mPostData, mListener); 196 } catch (android.net.ParseException ex) { 197 error = EventHandler.ERROR_BAD_URL; 198 } catch (java.lang.RuntimeException ex) { 199 /* probably an empty header set by javascript. We want 200 the same result as bad URL */ 201 error = EventHandler.ERROR_BAD_URL; 202 } 203 if (!ret) { 204 mListener.error(error, mListener.getContext().getText( 205 EventHandler.errorStringResources[Math.abs(error)]).toString()); 206 return false; 207 } 208 return true; 209 } 210 211 /* 212 * This function is used by handleCache to 213 * setup a load from the byte stream in a CacheResult. 214 */ startCacheLoad(CacheResult result)215 private void startCacheLoad(CacheResult result) { 216 if (DebugFlags.FRAME_LOADER) { 217 Log.v(LOGTAG, "FrameLoader: loading from cache: " 218 + mListener.url()); 219 } 220 // Tell the Listener respond with the cache file 221 CacheLoader cacheLoader = 222 new CacheLoader(mListener, result); 223 mListener.setCacheLoader(cacheLoader); 224 cacheLoader.load(); 225 } 226 227 /* 228 * This function is used by the handleHTTPLoad to setup the cache headers 229 * correctly. 230 * Returns true if the response was handled from the cache 231 */ handleCache()232 private boolean handleCache() { 233 switch (mCacheMode) { 234 // This mode is normally used for a reload, it instructs the http 235 // loader to not use the cached content. 236 case WebSettings.LOAD_NO_CACHE: 237 break; 238 239 240 // This mode is used when the content should only be loaded from 241 // the cache. If it is not there, then fail the load. This is used 242 // to load POST content in a history navigation. 243 case WebSettings.LOAD_CACHE_ONLY: { 244 CacheResult result = CacheManager.getCacheFile(mListener.url(), 245 null); 246 if (result != null) { 247 startCacheLoad(result); 248 } else { 249 // This happens if WebCore was first told that the POST 250 // response was in the cache, then when we try to use it 251 // it has gone. 252 // Generate a file not found error 253 int err = EventHandler.FILE_NOT_FOUND_ERROR; 254 mListener.error(err, mListener.getContext().getText( 255 EventHandler.errorStringResources[Math.abs(err)]) 256 .toString()); 257 } 258 return true; 259 } 260 261 // This mode is for when the user is doing a history navigation 262 // in the browser and should returned cached content regardless 263 // of it's state. If it is not in the cache, then go to the 264 // network. 265 case WebSettings.LOAD_CACHE_ELSE_NETWORK: { 266 if (DebugFlags.FRAME_LOADER) { 267 Log.v(LOGTAG, "FrameLoader: checking cache: " 268 + mListener.url()); 269 } 270 // Get the cache file name for the current URL, passing null for 271 // the validation headers causes no validation to occur 272 CacheResult result = CacheManager.getCacheFile(mListener.url(), 273 null); 274 if (result != null) { 275 startCacheLoad(result); 276 return true; 277 } 278 break; 279 } 280 281 // This is the default case, which is to check to see if the 282 // content in the cache can be used. If it can be used, then 283 // use it. If it needs revalidation then the relevant headers 284 // are added to the request. 285 default: 286 case WebSettings.LOAD_NORMAL: 287 return mListener.checkCache(mHeaders); 288 }// end of switch 289 290 return false; 291 } 292 293 /** 294 * Add the static headers that don't change with each request. 295 */ populateStaticHeaders()296 private void populateStaticHeaders() { 297 // Accept header should already be there as they are built by WebCore, 298 // but in the case they are missing, add some. 299 String accept = mHeaders.get("Accept"); 300 if (accept == null || accept.length() == 0) { 301 mHeaders.put("Accept", HEADER_STR); 302 } 303 mHeaders.put("Accept-Charset", "utf-8, iso-8859-1, utf-16, *;q=0.7"); 304 305 String acceptLanguage = mSettings.getAcceptLanguage(); 306 if (acceptLanguage.length() > 0) { 307 mHeaders.put("Accept-Language", acceptLanguage); 308 } 309 310 mHeaders.put("User-Agent", mSettings.getUserAgentString()); 311 } 312 313 /** 314 * Add the content related headers. These headers contain user private data 315 * and is not used when we are proxying an untrusted request. 316 */ populateHeaders()317 private void populateHeaders() { 318 319 if (mReferrer != null) mHeaders.put("Referer", mReferrer); 320 if (mContentType != null) mHeaders.put(CONTENT_TYPE, mContentType); 321 322 // if we have an active proxy and have proxy credentials, do pre-emptive 323 // authentication to avoid an extra round-trip: 324 if (mNetwork.isValidProxySet()) { 325 String username; 326 String password; 327 /* The proxy credentials can be set in the Network thread */ 328 synchronized (mNetwork) { 329 username = mNetwork.getProxyUsername(); 330 password = mNetwork.getProxyPassword(); 331 } 332 if (username != null && password != null) { 333 // we collect credentials ONLY if the proxy scheme is BASIC!!! 334 String proxyHeader = RequestHandle.authorizationHeader(true); 335 mHeaders.put(proxyHeader, 336 "Basic " + RequestHandle.computeBasicAuthResponse( 337 username, password)); 338 } 339 } 340 341 // Set cookie header 342 String cookie = CookieManager.getInstance().getCookie( 343 mListener.getWebAddress()); 344 if (cookie != null && cookie.length() > 0) { 345 mHeaders.put("Cookie", cookie); 346 } 347 } 348 } 349