Fix breakages in https://codereview.chromium.org/1155713003/
[chromium-blink-merge.git] / ios / web / net / crw_url_verifying_protocol_handler.mm
blob3200db117daf58a9976b10100f2b9c699bb9573a
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"
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 stringWithFormat:
120         @"try{"
121          "if(!window.__gCrWeb_CachedRequest||"
122          "!(window.__gCrWeb_CachedRequestDocument===window.document)){"
123          "window.__gCrWeb_CachedRequest = new XMLHttpRequest();"
124          "window.__gCrWeb_CachedRequestDocument = window.document;"
125          "}"
126          "window.__gCrWeb_CachedRequest.open('POST','%s',false);"
127          "window.__gCrWeb_CachedRequest.send();"
128          "}catch(e){"
129          "try{"
130          "window.__gCrWeb_CachedRequest.open('POST','%s',false);"
131          "window.__gCrWeb_CachedRequest.send();"
132          "}catch(e2){}"
133          "}"
134          "window.location.href",
135         web::kURLForVerification, kCheckRelativeURL] retain]);
136   }
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();
145   const GURL result(
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];
154   return result;
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
182   // thread too.
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
189   // executed.
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);
210   }
212   if (!nativeURL.is_valid()) {
213     *trustLevel = web::URLVerificationTrustLevel::kNone;
214     return jsURL;
215   }
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;
221     return nativeURL;
222   }
223   *trustLevel = web::URLVerificationTrustLevel::kAbsolute;
224   return jsURL;
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
231     // thread.
232     [[CRWURLVerifyingProtocolHandlerData sharedInstance]
233         swapLastSeenURL:net::GURLWithNSURL(request.mainDocumentURL)];
234   }
237 #pragma mark -
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])
245     return YES;
246   web::URLVerificationTrustLevel trustLevel;
247   web::WebClient* web_client = web::GetWebClient();
248   DCHECK(web_client);
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) {
261     return NO;
262   }
264   if (![[CRWURLVerifyingProtocolHandlerData
265               sharedInstance] runInInitWithRequest]) {
266     [CRWURLVerifyingProtocolHandler updateLastSeenUrl:request];
267   }
269   return YES;
272 + (NSURLRequest*)canonicalRequestForRequest:(NSURLRequest*)request {
273   return request;
276 - (id)initWithRequest:(NSURLRequest*)request
277        cachedResponse:(NSCachedURLResponse*)cachedResponse
278                client:(id<NSURLProtocolClient>)client {
279   if ((self = [super initWithRequest:request
280                       cachedResponse:cachedResponse
281                               client:client])) {
282     if ([[CRWURLVerifyingProtocolHandlerData
283                 sharedInstance] runInInitWithRequest]) {
284       [CRWURLVerifyingProtocolHandler updateLastSeenUrl:request];
285     }
287     _url.reset([request.URL retain]);
288   }
289   return self;
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]
297        initWithURL:_url
298         statusCode:200
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 {
309   // Nothing to do.
312 @end