cygprofile: increase timeouts to allow showing web contents
[chromium-blink-merge.git] / ios / chrome / browser / signin / gaia_auth_fetcher_ios.mm
blob88bcb748cdadf8d83fa865ba95775da1d4a769a3
1 // Copyright 2015 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 #include "ios/chrome/browser/signin/gaia_auth_fetcher_ios.h"
7 #import <WebKit/WebKit.h>
9 #include "base/json/string_escape.h"
10 #include "base/logging.h"
11 #import "base/mac/foundation_util.h"
12 #include "base/mac/scoped_block.h"
13 #import "base/mac/scoped_nsobject.h"
14 #include "base/strings/sys_string_conversions.h"
15 #include "ios/chrome/browser/experimental_flags.h"
16 #include "ios/chrome/browser/signin/gaia_auth_fetcher_ios_private.h"
17 #include "ios/web/public/browser_state.h"
18 #import "ios/web/public/web_view_creation_util.h"
19 #include "net/base/load_flags.h"
20 #import "net/base/mac/url_conversions.h"
21 #include "net/base/net_errors.h"
22 #include "net/http/http_request_headers.h"
23 #include "net/url_request/url_request_status.h"
25 namespace {
27 // Whether the iOS specialization of the GaiaAuthFetcher should be used.
28 bool g_should_use_gaia_auth_fetcher_ios = true;
30 // JavaScript template to do a POST request using an XMLHttpRequest.
31 // The request is retried once on failure, as it can be marked as failing to
32 // load the resource because of 302s on POST request (the cookies of the first
33 // response are correctly set).
35 // The template takes three arguments (in order):
36 // * The quoted and escaped URL to send a POST request to.
37 // * The HTTP headers of the request. They should be written as valid JavaScript
38 //   statements, adding headers to the XMLHttpRequest variable named 'req'
39 //   (e.g. 'req.setRequestHeader("Foo", "Bar");').
40 // * The quoted and escaped body of the POST request.
41 NSString* const kPostRequestTemplate =
42     @"<html><script>"
43      "function __gCrWebDoPostRequest() {"
44      "  function createAndSendPostRequest() {"
45      "    var req = new XMLHttpRequest();"
46      "    req.open(\"POST\", %@, false);"
47      "    req.setRequestHeader(\"Content-Type\","
48      "\"application/x-www-form-urlencoded\");"
49      "%@"
50      "    req.send(%@);"
51      "    if (req.status != 200) {"
52      "      throw req.status;"
53      "    }"
54      "    return req.responseText;"
55      "  }"
56      "  try {"
57      "    return createAndSendPostRequest();"
58      "  } catch(err) {"
59      "    return createAndSendPostRequest();"
60      "  }"
61      "}"
62      "</script></html>";
64 // JavaScript template to read the reponse to a GET or POST request. There is
65 // two different cases:
66 // * GET request, which was made by simply loading a request to the correct
67 //   URL. The response is the inner text (to avoid formatting in case of JSON
68 //   answers) of the body.
69 // * POST request, in case the "__gCrWebDoPostRequest" function is defined.
70 //   Running the function will do a POST request via a XMLHttpRequest and
71 //   return the response. See DoPostRequest below to know why this is necessary.
72 NSString* const kReadResponseTemplate =
73     @"if (typeof __gCrWebDoPostRequest === 'function') {"
74      "  __gCrWebDoPostRequest();"
75      "} else {"
76      "  document.body.innerText;"
77      "}";
79 // Creates an NSURLRequest to |url| that can be loaded by a WebView from |body|
80 // and |headers|.
81 // The request is a GET if |body| is empty and a POST otherwise.
82 NSURLRequest* GetRequest(const std::string& body,
83                          const std::string& headers,
84                          const GURL& url) {
85   base::scoped_nsobject<NSMutableURLRequest> request(
86       [[NSMutableURLRequest alloc] initWithURL:net::NSURLWithGURL(url)]);
87   net::HttpRequestHeaders request_headers;
88   request_headers.AddHeadersFromString(headers);
89   for (net::HttpRequestHeaders::Iterator it(request_headers); it.GetNext();) {
90     [request setValue:base::SysUTF8ToNSString(it.value())
91         forHTTPHeaderField:base::SysUTF8ToNSString(it.name())];
92   }
93   if (!body.empty()) {
94     NSData* post_data =
95         [base::SysUTF8ToNSString(body) dataUsingEncoding:NSUTF8StringEncoding];
96     [request setHTTPBody:post_data];
97     [request setHTTPMethod:@"POST"];
98     DCHECK(![[request allHTTPHeaderFields] objectForKey:@"Content-Type"]);
99     [request setValue:@"application/x-www-form-urlencoded"
100         forHTTPHeaderField:@"Content-Type"];
101   }
102   return request.autorelease();
105 // Escapes and quotes |value| and converts the result to an NSString.
106 NSString* EscapeAndQuoteToNSString(const std::string& value) {
107   return base::SysUTF8ToNSString(base::GetQuotedJSONString(value));
110 // Simulates a POST request on |web_view| using a XMLHttpRequest in
111 // JavaScript.
112 // This is needed because WKWebView ignores the HTTPBody in a POST request.
113 // See
114 // https://bugs.webkit.org/show_bug.cgi?id=145410
115 void DoPostRequest(WKWebView* web_view,
116                    const std::string& body,
117                    const std::string& headers,
118                    const GURL& url) {
119   GURL origin_url = url;
120   NSMutableString* header_data = [NSMutableString string];
121   net::HttpRequestHeaders request_headers;
122   request_headers.AddHeadersFromString(headers);
123   for (net::HttpRequestHeaders::Iterator it(request_headers); it.GetNext();) {
124     if (it.name() == "Origin") {
125       // The Origin request header cannot be set on an XMLHttpRequest. Set it
126       // by loading the script as if it was at the origin URL.
127       origin_url = GURL(it.value());
128       continue;
129     }
130     // net::HttpRequestHeaders escapes the name and value for a header. Some
131     // escaping might still be necessary for the JavaScript layer.
132     [header_data appendFormat:@"req.setRequestHeader(%@, %@);",
133                               EscapeAndQuoteToNSString(it.name()),
134                               EscapeAndQuoteToNSString(it.value())];
135   }
136   NSString* html_string =
137       [NSString stringWithFormat:kPostRequestTemplate,
138                                  EscapeAndQuoteToNSString(url.spec()),
139                                  header_data, EscapeAndQuoteToNSString(body)];
140   [web_view loadHTMLString:html_string baseURL:net::NSURLWithGURL(origin_url)];
142 }  // namespace
144 #pragma mark - GaiaAuthFetcherNavigationDelegate
146 @implementation GaiaAuthFetcherNavigationDelegate {
147   GaiaAuthFetcherIOSBridge* bridge_;  // weak
150 - (instancetype)initWithBridge:(GaiaAuthFetcherIOSBridge*)bridge {
151   self = [super init];
152   if (self) {
153     bridge_ = bridge;
154   }
155   return self;
158 #pragma mark WKNavigationDelegate
160 - (void)webView:(WKWebView*)webView
161     didFailNavigation:(WKNavigation*)navigation
162             withError:(NSError*)error {
163   DVLOG(1) << "Gaia fetcher navigation failed: "
164            << base::SysNSStringToUTF8(error.localizedDescription);
165   bridge_->URLFetchFailure(false /* is_cancelled */);
168 - (void)webView:(WKWebView*)webView
169     didFailProvisionalNavigation:(WKNavigation*)navigation
170                        withError:(NSError*)error {
171   DVLOG(1) << "Gaia fetcher provisional navigation failed: "
172            << base::SysNSStringToUTF8(error.localizedDescription);
173   bridge_->URLFetchFailure(false /* is_cancelled */);
176 - (void)webView:(WKWebView*)webView
177     didFinishNavigation:(WKNavigation*)navigation {
178   // A WKNavigation is an opaque object. The only way to access the body of the
179   // response is via Javascript.
180   DVLOG(2) << "WKWebView loaded:" << net::GURLWithNSURL(webView.URL);
181   [webView evaluateJavaScript:kReadResponseTemplate
182             completionHandler:^(NSString* result, NSError* error) {
183               if (error || !result) {
184                 DVLOG(1) << "Gaia fetcher extract body failed:"
185                          << base::SysNSStringToUTF8(error.localizedDescription);
186                 bridge_->URLFetchFailure(false /* is_cancelled */);
187               } else {
188                 DCHECK([result isKindOfClass:[NSString class]]);
189                 bridge_->URLFetchSuccess(base::SysNSStringToUTF8(result));
190               }
191             }];
194 @end
196 #pragma mark - GaiaAuthFetcherIOSBridge::Request
198 GaiaAuthFetcherIOSBridge::Request::Request()
199     : pending(false), url(), headers(), body() {}
201 GaiaAuthFetcherIOSBridge::Request::Request(const GURL& request_url,
202                                            const std::string& request_headers,
203                                            const std::string& request_body)
204     : pending(true),
205       url(request_url),
206       headers(request_headers),
207       body(request_body) {}
209 #pragma mark - GaiaAuthFetcherIOSBridge
211 GaiaAuthFetcherIOSBridge::GaiaAuthFetcherIOSBridge(
212     GaiaAuthFetcherIOS* fetcher,
213     web::BrowserState* browser_state)
214     : browser_state_(browser_state), fetcher_(fetcher), request_() {
215   web::BrowserState::GetActiveStateManager(browser_state_)->AddObserver(this);
218 GaiaAuthFetcherIOSBridge::~GaiaAuthFetcherIOSBridge() {
219   web::BrowserState::GetActiveStateManager(browser_state_)
220       ->RemoveObserver(this);
221   ResetWKWebView();
224 void GaiaAuthFetcherIOSBridge::Fetch(const GURL& url,
225                                      const std::string& headers,
226                                      const std::string& body) {
227   request_ = Request(url, headers, body);
228   FetchPendingRequest();
231 void GaiaAuthFetcherIOSBridge::Cancel() {
232   if (!request_.pending) {
233     return;
234   }
235   [GetWKWebView() stopLoading];
236   URLFetchFailure(true /* is_cancelled */);
239 void GaiaAuthFetcherIOSBridge::URLFetchSuccess(const std::string& data) {
240   if (!request_.pending) {
241     return;
242   }
243   GURL url = FinishPendingRequest();
244   // WKWebViewNavigationDelegate API doesn't give any way to get the HTTP
245   // response code of a navigation. Default to 200 for success.
246   fetcher_->FetchComplete(url, data, net::ResponseCookies(),
247                           net::URLRequestStatus(), 200);
250 void GaiaAuthFetcherIOSBridge::URLFetchFailure(bool is_cancelled) {
251   if (!request_.pending) {
252     return;
253   }
254   GURL url = FinishPendingRequest();
255   // WKWebViewNavigationDelegate API doesn't give any way to get the HTTP
256   // response code of a navigation. Default to 500 for error.
257   int error = is_cancelled ? net::ERR_ABORTED : net::ERR_FAILED;
258   fetcher_->FetchComplete(url, std::string(), net::ResponseCookies(),
259                           net::URLRequestStatus::FromError(error), 500);
262 void GaiaAuthFetcherIOSBridge::FetchPendingRequest() {
263   if (!request_.pending)
264     return;
265   if (!request_.body.empty()) {
266     DoPostRequest(GetWKWebView(), request_.body, request_.headers,
267                   request_.url);
268   } else {
269   [GetWKWebView()
270       loadRequest:GetRequest(request_.body, request_.headers, request_.url)];
271   }
274 GURL GaiaAuthFetcherIOSBridge::FinishPendingRequest() {
275   GURL url = request_.url;
276   request_ = Request();
277   return url;
280 WKWebView* GaiaAuthFetcherIOSBridge::GetWKWebView() {
281   if (!web::BrowserState::GetActiveStateManager(browser_state_)->IsActive()) {
282     // |browser_state_| is not active, WKWebView linked to this browser state
283     // should not exist or be created.
284     return nil;
285   }
286   if (!web_view_) {
287     web_view_.reset(CreateWKWebView());
288     navigation_delegate_.reset(
289         [[GaiaAuthFetcherNavigationDelegate alloc] initWithBridge:this]);
290     [web_view_ setNavigationDelegate:navigation_delegate_];
291   }
292   return web_view_.get();
295 void GaiaAuthFetcherIOSBridge::ResetWKWebView() {
296   [web_view_ setNavigationDelegate:nil];
297   [web_view_ stopLoading];
298   web_view_.reset();
299   navigation_delegate_.reset();
302 WKWebView* GaiaAuthFetcherIOSBridge::CreateWKWebView() {
303   return web::CreateWKWebView(CGRectZero, browser_state_);
306 void GaiaAuthFetcherIOSBridge::OnActive() {
307   // |browser_state_| is now active. If there is a pending request, restart it.
308   FetchPendingRequest();
311 void GaiaAuthFetcherIOSBridge::OnInactive() {
312   // |browser_state_| is now inactive. Stop using |web_view_| and don't create
313   // a new one until it is active.
314   ResetWKWebView();
317 #pragma mark - GaiaAuthFetcherIOS definition
319 GaiaAuthFetcherIOS::GaiaAuthFetcherIOS(GaiaAuthConsumer* consumer,
320                                        const std::string& source,
321                                        net::URLRequestContextGetter* getter,
322                                        web::BrowserState* browser_state)
323     : GaiaAuthFetcher(consumer, source, getter),
324       bridge_(new GaiaAuthFetcherIOSBridge(this, browser_state)),
325       browser_state_(browser_state) {}
327 GaiaAuthFetcherIOS::~GaiaAuthFetcherIOS() {
330 void GaiaAuthFetcherIOS::CreateAndStartGaiaFetcher(const std::string& body,
331                                                    const std::string& headers,
332                                                    const GURL& gaia_gurl,
333                                                    int load_flags) {
334   DCHECK(!HasPendingFetch()) << "Tried to fetch two things at once!";
336   bool cookies_required = !(load_flags & (net::LOAD_DO_NOT_SEND_COOKIES |
337                                           net::LOAD_DO_NOT_SAVE_COOKIES));
338   if (!ShouldUseGaiaAuthFetcherIOS() || !cookies_required) {
339     GaiaAuthFetcher::CreateAndStartGaiaFetcher(body, headers, gaia_gurl,
340                                                load_flags);
341     return;
342   }
344   DVLOG(2) << "Gaia fetcher URL: " << gaia_gurl.spec();
345   DVLOG(2) << "Gaia fetcher headers: " << headers;
346   DVLOG(2) << "Gaia fetcher body: " << body;
348   // The fetch requires cookies and WKWebView is being used. The only way to do
349   // a network request with cookies sent and saved is by making it through a
350   // WKWebView.
351   SetPendingFetch(true);
352   bridge_->Fetch(gaia_gurl, headers, body);
355 void GaiaAuthFetcherIOS::CancelRequest() {
356   if (!HasPendingFetch()) {
357     return;
358   }
359   bridge_->Cancel();
360   GaiaAuthFetcher::CancelRequest();
363 void GaiaAuthFetcherIOS::FetchComplete(const GURL& url,
364                                        const std::string& data,
365                                        const net::ResponseCookies& cookies,
366                                        const net::URLRequestStatus& status,
367                                        int response_code) {
368   DVLOG(2) << "Response " << url.spec() << ", code = " << response_code << "\n";
369   DVLOG(2) << "data: " << data << "\n";
370   SetPendingFetch(false);
371   DispatchFetchedRequest(url, data, cookies, status, response_code);
374 void GaiaAuthFetcherIOS::SetShouldUseGaiaAuthFetcherIOSForTesting(
375     bool use_gaia_fetcher_ios) {
376   g_should_use_gaia_auth_fetcher_ios = use_gaia_fetcher_ios;
379 bool GaiaAuthFetcherIOS::ShouldUseGaiaAuthFetcherIOS() {
380   return experimental_flags::IsWKWebViewEnabled() &&
381          g_should_use_gaia_auth_fetcher_ios;