1 // Copyright 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #import "ios/web/net/crw_url_verifying_protocol_handler.h"
7 #include "base/ios/ios_util.h"
8 #include "base/logging.h"
9 #include "base/mac/scoped_nsobject.h"
10 #include "base/metrics/histogram.h"
11 #include "base/strings/sys_string_conversions.h"
12 #include "base/time/time.h"
13 #include "ios/web/public/web_client.h"
14 #import "ios/web/web_state/web_view_creation_utils.h"
15 #import "net/base/mac/url_conversions.h"
18 // A private singleton object to hold all shared flags/data used by
19 // CRWURLVerifyingProtocolHandler.
20 @interface CRWURLVerifyingProtocolHandlerData : NSObject {
22 // Flag to remember that class has been pre-initialized.
24 // Contains the last seen URL by the constructor of the ProtocolHandler.
25 // This must only be accessed from the main thread.
27 // On iOS8, |+canInitWithRequest| is not called on the main thread. Thus the
28 // url check is run in |-initWithRequest| instead. See crbug.com/380768.
29 // TODO(droger): Run the check at the same place on all versions.
30 BOOL _runInInitWithRequest;
32 @property(nonatomic, assign) BOOL preInitialized;
33 @property(nonatomic, readonly) BOOL runInInitWithRequest;
34 // Returns the global CRWURLVerifyingProtocolHandlerData instance.
35 + (CRWURLVerifyingProtocolHandlerData*)sharedInstance;
36 // If there is a URL saved as "last seen URL", return it as an autoreleased
37 // object. |newURL| is now saved as the "last seen URL".
38 - (GURL)swapLastSeenURL:(const GURL&)newURL;
41 @implementation CRWURLVerifyingProtocolHandlerData
42 @synthesize preInitialized = _preInitialized;
43 @synthesize runInInitWithRequest = _runInInitWithRequest;
45 + (CRWURLVerifyingProtocolHandlerData*)sharedInstance {
46 static CRWURLVerifyingProtocolHandlerData* instance =
47 [[CRWURLVerifyingProtocolHandlerData alloc] init];
51 - (GURL)swapLastSeenURL:(const GURL&)newURL {
52 // Note that release() does *not* call release on oldURL.
53 const GURL oldURL(_lastSeenURL);
54 _lastSeenURL = newURL;
58 - (instancetype)init {
59 if (self = [super init]) {
60 _runInInitWithRequest = base::ios::IsRunningOnIOS8OrLater();
69 // The special URL used to communicate between the JavaScript and this handler.
70 // This has to be a http request, because Ajax request can only be cross origin
71 // for http and https schemes. localhost:0 is used because no sane URL should
72 // use this. A relative URL is also used if the first request fails, because
73 // HTTP servers can use headers to prevent arbitrary ajax requests.
74 const char kURLForVerification[] = "https://localhost:0/crwebiossecurity";
80 // This URL has been chosen with a specific prefix, and a random suffix to
81 // prevent accidental collision.
82 const char kCheckRelativeURL[] =
83 "/crwebiossecurity/b86b97a1-2ce0-44fd-a074-e2158790c98d";
87 @interface CRWURLVerifyingProtocolHandler () {
88 // The URL of the request to handle.
89 base::scoped_nsobject<NSURL> _url;
92 // Returns the JavaScript to execute to check URL.
93 + (NSString*)checkURLJavaScript;
94 // Implements the logic for verifying the current URL for the given
95 // |webView|. This is the internal implementation for the public interface
96 // of +currentURLForWebView:trustLevel:.
97 + (GURL)internalCurrentURLForWebView:(UIWebView*)webView
98 trustLevel:(web::URLVerificationTrustLevel*)trustLevel;
99 // Updates the last seen URL to be the mainDocumentURL of |request|.
100 + (void)updateLastSeenUrl:(NSURLRequest*)request;
103 @implementation CRWURLVerifyingProtocolHandler
105 + (NSString*)checkURLJavaScript {
106 static base::scoped_nsobject<NSString> cachedJavaScript;
107 if (!cachedJavaScript) {
108 // The JavaScript to execute. It does execute an synchronous Ajax request to
109 // the special URL handled by this handler and then returns the URL of the
110 // UIWebView by computing window.location.href.
112 // - Creating a new XMLHttpRequest can crash the application if the Web
113 // Thread is iterating over active DOM objects. To prevent this from
114 // happening, the same XMLHttpRequest is reused as much as possible.
115 // - A XMLHttpRequest is associated to a document, and trying to reuse one
116 // from another document will trigger an exception. To prevent this,
117 // information about the document on which the current XMLHttpRequest has
118 // been created is kept.
119 cachedJavaScript.reset([[NSString stringWithFormat:
121 "if(!window.__gCrWeb_CachedRequest||"
122 "!(window.__gCrWeb_CachedRequestDocument===window.document)){"
123 "window.__gCrWeb_CachedRequest = new XMLHttpRequest();"
124 "window.__gCrWeb_CachedRequestDocument = window.document;"
126 "window.__gCrWeb_CachedRequest.open('POST','%s',false);"
127 "window.__gCrWeb_CachedRequest.send();"
130 "window.__gCrWeb_CachedRequest.open('POST','%s',false);"
131 "window.__gCrWeb_CachedRequest.send();"
134 "window.location.href",
135 web::kURLForVerification, kCheckRelativeURL] retain]);
137 return cachedJavaScript.get();
140 // Calls +internalCurrentURLForWebView:trustLevel: to do the real work.
141 // Logs timing of the actual work to console for debugging.
142 + (GURL)currentURLForWebView:(UIWebView*)webView
143 trustLevel:(web::URLVerificationTrustLevel*)trustLevel {
144 base::Time start = base::Time::NowFromSystemTime();
146 [CRWURLVerifyingProtocolHandler internalCurrentURLForWebView:webView
147 trustLevel:trustLevel]);
148 base::TimeDelta elapsed = base::Time::NowFromSystemTime() - start;
149 UMA_HISTOGRAM_TIMES("WebController.UrlVerifyTimes", elapsed);
150 // Setting pre-initialization flag to YES here disables pre-initialization
151 // if pre-initialization is held back such that it is called after the
152 // first real call to this function.
153 [[CRWURLVerifyingProtocolHandlerData sharedInstance] setPreInitialized:YES];
157 // The implementation of this method is doing the following
158 // - Set the "last seen URL" to nil.
159 // - Inject JavaScript in the UIWebView that will execute a synchronous ajax
160 // request to |kURLForVerification|
161 // - The CRWURLVerifyingProtocolHandler will then update "last seen URL" to
162 // the value of the request mainDocumentURL.
163 // - Execute window.location.href on the UIWebView.
164 // - Do one of the following:
165 // - If "last seen URL" is nil, return the value of window.location.href and
166 // set |trustLevel| to kNone.
167 // - If "last seen URL" is not nil and "last seen URL" origin is the same as
168 // window.location.href origin, return window.location.href and set
169 // |trustLevel| to kAbsolute.
170 // - If "last seen URL" is not nil and "last seen URL" origin is *not* the
171 // same as window.location.href origin, return "last seen URL" and set
172 // |trustLevel| to kMixed.
173 // Only the origin is checked, because pushed states are not reflected in the
174 // mainDocumentURL of the request.
175 + (GURL)internalCurrentURLForWebView:(UIWebView*)webView
176 trustLevel:(web::URLVerificationTrustLevel*)trustLevel {
177 // This should only be called on the main thread. The reason is that an
178 // attacker must not be able to compromise the result by generating a request
179 // to |kURLForVerification| from another tab. To prevent this,
180 // "last seen URL" is only updated if the handler is created on the main
181 // thread, and this only happens if the JavaScript is injected from the main
183 DCHECK([NSThread isMainThread]);
184 DCHECK(trustLevel) << "Verification of the trustLevel state is mandatory";
186 // Compute the main document URL using a synchronous AJAX request.
187 [[CRWURLVerifyingProtocolHandlerData sharedInstance] swapLastSeenURL:GURL()];
188 // Executing the script, will set "last seen URL" as a request will be
190 NSString* script = [CRWURLVerifyingProtocolHandler checkURLJavaScript];
191 NSString* href = [webView stringByEvaluatingJavaScriptFromString:script];
192 GURL nativeURL([[CRWURLVerifyingProtocolHandlerData sharedInstance]
193 swapLastSeenURL:GURL()]);
195 // applewebdata:// is occasionally set as the URL for a blank page during
196 // transition. For instance, if <META HTTP-EQUIV="refresh" ...>' is used.
197 // This results in spurious history entries if this isn't masked with the
198 // default page URL of about:blank.
199 if ([href hasPrefix:@"applewebdata://"])
200 href = @"about:blank";
201 const GURL jsURL(base::SysNSStringToUTF8(href));
203 // If XHR is not working (e.g., slow PDF, XHR blocked), fall back to the
204 // UIWebView request. This lags behind the other changes (it appears to update
205 // at the point where the document object becomes present), so it's more
206 // likely to return kMixed during transitions, but it's better than erroring
207 // out when the faster XHR validation method isn't available.
208 if (!nativeURL.is_valid() && webView.request) {
209 nativeURL = net::GURLWithNSURL(webView.request.URL);
212 if (!nativeURL.is_valid()) {
213 *trustLevel = web::URLVerificationTrustLevel::kNone;
216 if (jsURL.GetOrigin() != nativeURL.GetOrigin()) {
217 DVLOG(1) << "Origin differs, trusting webkit over JavaScript ["
218 << "jsURLOrigin='" << jsURL.GetOrigin() << ", "
219 << "nativeURLOrigin='" << nativeURL.GetOrigin() << "']";
220 *trustLevel = web::URLVerificationTrustLevel::kMixed;
223 *trustLevel = web::URLVerificationTrustLevel::kAbsolute;
227 + (void)updateLastSeenUrl:(NSURLRequest*)request {
228 DCHECK([NSThread isMainThread]);
229 if ([NSThread isMainThread]) {
230 // See above why this should only be done if this is called on the main
232 [[CRWURLVerifyingProtocolHandlerData sharedInstance]
233 swapLastSeenURL:net::GURLWithNSURL(request.mainDocumentURL)];
238 #pragma mark Class Method
240 // Injection of JavaScript into any UIWebView pre-initializes the entire
241 // system which will save run time when user types into Omnibox and triggers
242 // JavaScript injection again.
243 + (BOOL)preInitialize {
244 if ([[CRWURLVerifyingProtocolHandlerData sharedInstance] preInitialized])
246 web::URLVerificationTrustLevel trustLevel;
247 web::WebClient* web_client = web::GetWebClient();
249 base::scoped_nsobject<UIWebView> dummyWebView(web::CreateStaticFileWebView());
250 [CRWURLVerifyingProtocolHandler currentURLForWebView:dummyWebView
251 trustLevel:&trustLevel];
252 return [[CRWURLVerifyingProtocolHandlerData sharedInstance] preInitialized];
255 #pragma mark NSURLProtocol methods
257 + (BOOL)canInitWithRequest:(NSURLRequest*)request {
258 GURL requestURL = net::GURLWithNSURL(request.URL);
259 if (requestURL != GURL(web::kURLForVerification) &&
260 requestURL.path() != kCheckRelativeURL) {
264 if (![[CRWURLVerifyingProtocolHandlerData
265 sharedInstance] runInInitWithRequest]) {
266 [CRWURLVerifyingProtocolHandler updateLastSeenUrl:request];
272 + (NSURLRequest*)canonicalRequestForRequest:(NSURLRequest*)request {
276 - (id)initWithRequest:(NSURLRequest*)request
277 cachedResponse:(NSCachedURLResponse*)cachedResponse
278 client:(id<NSURLProtocolClient>)client {
279 if ((self = [super initWithRequest:request
280 cachedResponse:cachedResponse
282 if ([[CRWURLVerifyingProtocolHandlerData
283 sharedInstance] runInInitWithRequest]) {
284 [CRWURLVerifyingProtocolHandler updateLastSeenUrl:request];
287 _url.reset([request.URL retain]);
292 - (void)startLoading {
293 NSMutableDictionary* headerFields = [NSMutableDictionary dictionary];
294 // This request is done by an AJAX call, cross origin must be allowed.
295 [headerFields setObject:@"*" forKey:@"Access-Control-Allow-Origin"];
296 base::scoped_nsobject<NSHTTPURLResponse> response([[NSHTTPURLResponse alloc]
299 HTTPVersion:@"HTTP/1.1"
300 headerFields:headerFields]);
301 [self.client URLProtocol:self
302 didReceiveResponse:response
303 cacheStoragePolicy:NSURLCacheStorageNotAllowed];
304 [self.client URLProtocol:self didLoadData:[NSData data]];
305 [self.client URLProtocolDidFinishLoading:self];
308 - (void)stopLoading {