Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / ios / web / net / crw_url_verifying_protocol_handler.mm
blob492bef880a6b0ce7110e2f95d68787d5aaca78f5
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"
16 #include "url/gurl.h"
18 // A private singleton object to hold all shared flags/data used by
19 // CRWURLVerifyingProtocolHandler.
20 @interface CRWURLVerifyingProtocolHandlerData : NSObject {
21  @private
22   // Flag to remember that class has been pre-initialized.
23   BOOL _preInitialized;
24   // Contains the last seen URL by the constructor of the ProtocolHandler.
25   // This must only be accessed from the main thread.
26   GURL _lastSeenURL;
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;
39 @end
41 @implementation CRWURLVerifyingProtocolHandlerData
42 @synthesize preInitialized = _preInitialized;
43 @synthesize runInInitWithRequest = _runInInitWithRequest;
45 + (CRWURLVerifyingProtocolHandlerData*)sharedInstance {
46   static CRWURLVerifyingProtocolHandlerData* instance =
47       [[CRWURLVerifyingProtocolHandlerData alloc] init];
48   return instance;
51 - (GURL)swapLastSeenURL:(const GURL&)newURL {
52   // Note that release() does *not* call release on oldURL.
53   const GURL oldURL(_lastSeenURL);
54   _lastSeenURL = newURL;
55   return oldURL;
58 - (instancetype)init {
59   if (self = [super init]) {
60     _runInInitWithRequest = base::ios::IsRunningOnIOS8OrLater();
61   }
62   return self;
65 @end
67 namespace web {
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";
76 }  // namespace web
78 namespace {
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";
85 }  // namespace
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;
101 @end
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.
111     // NOTE(qsr):
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
120         stringWithFormat:
121             @"try{"
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;"
127              "}"
128              "window.__gCrWeb_CachedRequest.open('POST','%s',false);"
129              "window.__gCrWeb_CachedRequest.send();"
130              "}catch(e){"
131              "try{"
132              "window.__gCrWeb_CachedRequest.open('POST','%s',false);"
133              "window.__gCrWeb_CachedRequest.send();"
134              "}catch(e2){}"
135              "}"
136              "delete window.__gCrWeb_Verifying;"
137              "window.location.href",
138             web::kURLForVerification, kCheckRelativeURL] retain]);
139   }
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();
148   const GURL result(
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];
157   return result;
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
185   // thread too.
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
192   // executed.
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);
213   }
215   if (!nativeURL.is_valid()) {
216     *trustLevel = web::URLVerificationTrustLevel::kNone;
217     return jsURL;
218   }
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;
224     return nativeURL;
225   }
226   *trustLevel = web::URLVerificationTrustLevel::kAbsolute;
227   return jsURL;
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
234     // thread.
235     [[CRWURLVerifyingProtocolHandlerData sharedInstance]
236         swapLastSeenURL:net::GURLWithNSURL(request.mainDocumentURL)];
237   }
240 #pragma mark -
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])
248     return YES;
249   web::URLVerificationTrustLevel trustLevel;
250   web::WebClient* web_client = web::GetWebClient();
251   DCHECK(web_client);
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) {
264     return NO;
265   }
267   if (![[CRWURLVerifyingProtocolHandlerData
268               sharedInstance] runInInitWithRequest]) {
269     [CRWURLVerifyingProtocolHandler updateLastSeenUrl:request];
270   }
272   return YES;
275 + (NSURLRequest*)canonicalRequestForRequest:(NSURLRequest*)request {
276   return request;
279 - (id)initWithRequest:(NSURLRequest*)request
280        cachedResponse:(NSCachedURLResponse*)cachedResponse
281                client:(id<NSURLProtocolClient>)client {
282   if ((self = [super initWithRequest:request
283                       cachedResponse:cachedResponse
284                               client:client])) {
285     if ([[CRWURLVerifyingProtocolHandlerData
286                 sharedInstance] runInInitWithRequest]) {
287       [CRWURLVerifyingProtocolHandler updateLastSeenUrl:request];
288     }
290     _url.reset([request.URL retain]);
291   }
292   return self;
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]
300        initWithURL:_url
301         statusCode:200
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 {
312   // Nothing to do.
315 @end