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_internal_creation_util.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
122 "window.__gCrWeb_Verifying = true;"
123 "if(!window.__gCrWeb_CachedRequest||"
124 "!(window.__gCrWeb_CachedRequestDocument===window.document)){"
125 "window.__gCrWeb_CachedRequest = new XMLHttpRequest();"
126 "window.__gCrWeb_CachedRequestDocument = window.document;"
128 "window.__gCrWeb_CachedRequest.open('POST','%s',false);"
129 "window.__gCrWeb_CachedRequest.send();"
132 "window.__gCrWeb_CachedRequest.open('POST','%s',false);"
133 "window.__gCrWeb_CachedRequest.send();"
136 "delete window.__gCrWeb_Verifying;"
137 "window.location.href",
138 web::kURLForVerification, kCheckRelativeURL] retain]);
140 return cachedJavaScript.get();
143 // Calls +internalCurrentURLForWebView:trustLevel: to do the real work.
144 // Logs timing of the actual work to console for debugging.
145 + (GURL)currentURLForWebView:(UIWebView*)webView
146 trustLevel:(web::URLVerificationTrustLevel*)trustLevel {
147 base::Time start = base::Time::NowFromSystemTime();
149 [CRWURLVerifyingProtocolHandler internalCurrentURLForWebView:webView
150 trustLevel:trustLevel]);
151 base::TimeDelta elapsed = base::Time::NowFromSystemTime() - start;
152 UMA_HISTOGRAM_TIMES("WebController.UrlVerifyTimes", elapsed);
153 // Setting pre-initialization flag to YES here disables pre-initialization
154 // if pre-initialization is held back such that it is called after the
155 // first real call to this function.
156 [[CRWURLVerifyingProtocolHandlerData sharedInstance] setPreInitialized:YES];
160 // The implementation of this method is doing the following
161 // - Set the "last seen URL" to nil.
162 // - Inject JavaScript in the UIWebView that will execute a synchronous ajax
163 // request to |kURLForVerification|
164 // - The CRWURLVerifyingProtocolHandler will then update "last seen URL" to
165 // the value of the request mainDocumentURL.
166 // - Execute window.location.href on the UIWebView.
167 // - Do one of the following:
168 // - If "last seen URL" is nil, return the value of window.location.href and
169 // set |trustLevel| to kNone.
170 // - If "last seen URL" is not nil and "last seen URL" origin is the same as
171 // window.location.href origin, return window.location.href and set
172 // |trustLevel| to kAbsolute.
173 // - If "last seen URL" is not nil and "last seen URL" origin is *not* the
174 // same as window.location.href origin, return "last seen URL" and set
175 // |trustLevel| to kMixed.
176 // Only the origin is checked, because pushed states are not reflected in the
177 // mainDocumentURL of the request.
178 + (GURL)internalCurrentURLForWebView:(UIWebView*)webView
179 trustLevel:(web::URLVerificationTrustLevel*)trustLevel {
180 // This should only be called on the main thread. The reason is that an
181 // attacker must not be able to compromise the result by generating a request
182 // to |kURLForVerification| from another tab. To prevent this,
183 // "last seen URL" is only updated if the handler is created on the main
184 // thread, and this only happens if the JavaScript is injected from the main
186 DCHECK([NSThread isMainThread]);
187 DCHECK(trustLevel) << "Verification of the trustLevel state is mandatory";
189 // Compute the main document URL using a synchronous AJAX request.
190 [[CRWURLVerifyingProtocolHandlerData sharedInstance] swapLastSeenURL:GURL()];
191 // Executing the script, will set "last seen URL" as a request will be
193 NSString* script = [CRWURLVerifyingProtocolHandler checkURLJavaScript];
194 NSString* href = [webView stringByEvaluatingJavaScriptFromString:script];
195 GURL nativeURL([[CRWURLVerifyingProtocolHandlerData sharedInstance]
196 swapLastSeenURL:GURL()]);
198 // applewebdata:// is occasionally set as the URL for a blank page during
199 // transition. For instance, if <META HTTP-EQUIV="refresh" ...>' is used.
200 // This results in spurious history entries if this isn't masked with the
201 // default page URL of about:blank.
202 if ([href hasPrefix:@"applewebdata://"])
203 href = @"about:blank";
204 const GURL jsURL(base::SysNSStringToUTF8(href));
206 // If XHR is not working (e.g., slow PDF, XHR blocked), fall back to the
207 // UIWebView request. This lags behind the other changes (it appears to update
208 // at the point where the document object becomes present), so it's more
209 // likely to return kMixed during transitions, but it's better than erroring
210 // out when the faster XHR validation method isn't available.
211 if (!nativeURL.is_valid() && webView.request) {
212 nativeURL = net::GURLWithNSURL(webView.request.URL);
215 if (!nativeURL.is_valid()) {
216 *trustLevel = web::URLVerificationTrustLevel::kNone;
219 if (jsURL.GetOrigin() != nativeURL.GetOrigin()) {
220 DVLOG(1) << "Origin differs, trusting webkit over JavaScript ["
221 << "jsURLOrigin='" << jsURL.GetOrigin() << ", "
222 << "nativeURLOrigin='" << nativeURL.GetOrigin() << "']";
223 *trustLevel = web::URLVerificationTrustLevel::kMixed;
226 *trustLevel = web::URLVerificationTrustLevel::kAbsolute;
230 + (void)updateLastSeenUrl:(NSURLRequest*)request {
231 DCHECK([NSThread isMainThread]);
232 if ([NSThread isMainThread]) {
233 // See above why this should only be done if this is called on the main
235 [[CRWURLVerifyingProtocolHandlerData sharedInstance]
236 swapLastSeenURL:net::GURLWithNSURL(request.mainDocumentURL)];
241 #pragma mark Class Method
243 // Injection of JavaScript into any UIWebView pre-initializes the entire
244 // system which will save run time when user types into Omnibox and triggers
245 // JavaScript injection again.
246 + (BOOL)preInitialize {
247 if ([[CRWURLVerifyingProtocolHandlerData sharedInstance] preInitialized])
249 web::URLVerificationTrustLevel trustLevel;
250 web::WebClient* web_client = web::GetWebClient();
252 base::scoped_nsobject<UIWebView> dummyWebView(web::CreateStaticFileWebView());
253 [CRWURLVerifyingProtocolHandler currentURLForWebView:dummyWebView
254 trustLevel:&trustLevel];
255 return [[CRWURLVerifyingProtocolHandlerData sharedInstance] preInitialized];
258 #pragma mark NSURLProtocol methods
260 + (BOOL)canInitWithRequest:(NSURLRequest*)request {
261 GURL requestURL = net::GURLWithNSURL(request.URL);
262 if (requestURL != GURL(web::kURLForVerification) &&
263 requestURL.path() != kCheckRelativeURL) {
267 if (![[CRWURLVerifyingProtocolHandlerData
268 sharedInstance] runInInitWithRequest]) {
269 [CRWURLVerifyingProtocolHandler updateLastSeenUrl:request];
275 + (NSURLRequest*)canonicalRequestForRequest:(NSURLRequest*)request {
279 - (id)initWithRequest:(NSURLRequest*)request
280 cachedResponse:(NSCachedURLResponse*)cachedResponse
281 client:(id<NSURLProtocolClient>)client {
282 if ((self = [super initWithRequest:request
283 cachedResponse:cachedResponse
285 if ([[CRWURLVerifyingProtocolHandlerData
286 sharedInstance] runInInitWithRequest]) {
287 [CRWURLVerifyingProtocolHandler updateLastSeenUrl:request];
290 _url.reset([request.URL retain]);
295 - (void)startLoading {
296 NSMutableDictionary* headerFields = [NSMutableDictionary dictionary];
297 // This request is done by an AJAX call, cross origin must be allowed.
298 [headerFields setObject:@"*" forKey:@"Access-Control-Allow-Origin"];
299 base::scoped_nsobject<NSHTTPURLResponse> response([[NSHTTPURLResponse alloc]
302 HTTPVersion:@"HTTP/1.1"
303 headerFields:headerFields]);
304 [self.client URLProtocol:self
305 didReceiveResponse:response
306 cacheStoragePolicy:NSURLCacheStorageNotAllowed];
307 [self.client URLProtocol:self didLoadData:[NSData data]];
308 [self.client URLProtocolDidFinishLoading:self];
311 - (void)stopLoading {