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"
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 =
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\");"
51 " if (req.status != 200) {"
54 " return req.responseText;"
57 " return createAndSendPostRequest();"
59 " return createAndSendPostRequest();"
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();"
76 " document.body.innerText;"
79 // Creates an NSURLRequest to |url| that can be loaded by a WebView from |body|
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,
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())];
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"];
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
112 // This is needed because WKWebView ignores the HTTPBody in a POST request.
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,
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());
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())];
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)];
144 #pragma mark - GaiaAuthFetcherNavigationDelegate
146 @implementation GaiaAuthFetcherNavigationDelegate {
147 GaiaAuthFetcherIOSBridge* bridge_; // weak
150 - (instancetype)initWithBridge:(GaiaAuthFetcherIOSBridge*)bridge {
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 */);
188 DCHECK([result isKindOfClass:[NSString class]]);
189 bridge_->URLFetchSuccess(base::SysNSStringToUTF8(result));
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)
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);
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) {
235 [GetWKWebView() stopLoading];
236 URLFetchFailure(true /* is_cancelled */);
239 void GaiaAuthFetcherIOSBridge::URLFetchSuccess(const std::string& data) {
240 if (!request_.pending) {
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) {
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)
265 if (!request_.body.empty()) {
266 DoPostRequest(GetWKWebView(), request_.body, request_.headers,
270 loadRequest:GetRequest(request_.body, request_.headers, request_.url)];
274 GURL GaiaAuthFetcherIOSBridge::FinishPendingRequest() {
275 GURL url = request_.url;
276 request_ = Request();
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.
287 web_view_.reset(CreateWKWebView());
288 navigation_delegate_.reset(
289 [[GaiaAuthFetcherNavigationDelegate alloc] initWithBridge:this]);
290 [web_view_ setNavigationDelegate:navigation_delegate_];
292 return web_view_.get();
295 void GaiaAuthFetcherIOSBridge::ResetWKWebView() {
296 [web_view_ setNavigationDelegate:nil];
297 [web_view_ stopLoading];
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.
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,
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,
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
351 SetPendingFetch(true);
352 bridge_->Fetch(gaia_gurl, headers, body);
355 void GaiaAuthFetcherIOS::CancelRequest() {
356 if (!HasPendingFetch()) {
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,
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;