blob: d701444d4e28c82697f4fa63f7cd947c923f1ca7 [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ios/chrome/browser/signin/gaia_auth_fetcher_ios.h"
#import <WebKit/WebKit.h>
#include "base/ios/ios_util.h"
#include "base/json/string_escape.h"
#include "base/logging.h"
#import "base/mac/foundation_util.h"
#include "base/mac/scoped_block.h"
#include "base/strings/sys_string_conversions.h"
#include "ios/chrome/browser/signin/gaia_auth_fetcher_ios_private.h"
#include "ios/web/public/browser_state.h"
#import "ios/web/public/web_view_creation_util.h"
#include "net/base/load_flags.h"
#import "net/base/mac/url_conversions.h"
#include "net/base/net_errors.h"
#include "net/http/http_request_headers.h"
#include "net/url_request/url_request_status.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
// Whether the iOS specialization of the GaiaAuthFetcher should be used.
bool g_should_use_gaia_auth_fetcher_ios = true;
// JavaScript template to do a POST request using an XMLHttpRequest.
// The request is retried once on failure, as it can be marked as failing to
// load the resource because of 302s on POST request (the cookies of the first
// response are correctly set).
//
// The template takes three arguments (in order):
// * The quoted and escaped URL to send a POST request to.
// * The HTTP headers of the request. They should be written as valid JavaScript
// statements, adding headers to the XMLHttpRequest variable named 'req'
// (e.g. 'req.setRequestHeader("Foo", "Bar");').
// * The quoted and escaped body of the POST request.
NSString* const kPostRequestTemplate =
@"<html><script>"
"function __gCrWebDoPostRequest() {"
" function createAndSendPostRequest() {"
" var req = new XMLHttpRequest();"
" req.open(\"POST\", %@, false);"
" req.setRequestHeader(\"Content-Type\","
"\"application/x-www-form-urlencoded\");"
"%@"
" req.send(%@);"
" if (req.status != 200) {"
" throw req.status;"
" }"
" return req.responseText;"
" }"
" try {"
" return createAndSendPostRequest();"
" } catch(err) {"
" return createAndSendPostRequest();"
" }"
"}"
"</script></html>";
// JavaScript template to read the response to a GET or POST request. There is
// two different cases:
// * GET request, which was made by simply loading a request to the correct
// URL. The response is the inner text (to avoid formatting in case of JSON
// answers) of the body.
// * POST request, in case the "__gCrWebDoPostRequest" function is defined.
// Running the function will do a POST request via a XMLHttpRequest and
// return the response. See DoPostRequest below to know why this is necessary.
NSString* const kReadResponseTemplate =
@"if (typeof __gCrWebDoPostRequest === 'function') {"
" __gCrWebDoPostRequest();"
"} else {"
" document.body.innerText;"
"}";
// Creates an NSURLRequest to |url| that can be loaded by a WebView from |body|
// and |headers|.
// The request is a GET if |body| is empty and a POST otherwise.
NSURLRequest* GetRequest(const std::string& body,
const std::string& headers,
const GURL& url) {
NSMutableURLRequest* request =
[[NSMutableURLRequest alloc] initWithURL:net::NSURLWithGURL(url)];
net::HttpRequestHeaders request_headers;
request_headers.AddHeadersFromString(headers);
for (net::HttpRequestHeaders::Iterator it(request_headers); it.GetNext();) {
[request setValue:base::SysUTF8ToNSString(it.value())
forHTTPHeaderField:base::SysUTF8ToNSString(it.name())];
}
if (!body.empty()) {
NSData* post_data =
[base::SysUTF8ToNSString(body) dataUsingEncoding:NSUTF8StringEncoding];
[request setHTTPBody:post_data];
[request setHTTPMethod:@"POST"];
DCHECK(![[request allHTTPHeaderFields] objectForKey:@"Content-Type"]);
[request setValue:@"application/x-www-form-urlencoded"
forHTTPHeaderField:@"Content-Type"];
}
return request;
}
// Escapes and quotes |value| and converts the result to an NSString.
NSString* EscapeAndQuoteToNSString(const std::string& value) {
return base::SysUTF8ToNSString(base::GetQuotedJSONString(value));
}
// Simulates a POST request on |web_view| using a XMLHttpRequest in
// JavaScript.
// This is needed because WKWebView ignores the HTTPBody in a POST request.
// See
// https://bugs.webkit.org/show_bug.cgi?id=145410
// TODO(crbug.com/740987): Remove this function workaround once iOS 10 is
// dropped.
void DoPostRequest(WKWebView* web_view,
const std::string& body,
const std::string& headers,
const GURL& url) {
DCHECK(!base::ios::IsRunningOnIOS11OrLater());
NSMutableString* header_data = [NSMutableString string];
net::HttpRequestHeaders request_headers;
request_headers.AddHeadersFromString(headers);
for (net::HttpRequestHeaders::Iterator it(request_headers); it.GetNext();) {
if (it.name() == "Origin") {
// The Origin request header cannot be set on an XMLHttpRequest.
continue;
}
// net::HttpRequestHeaders escapes the name and value for a header. Some
// escaping might still be necessary for the JavaScript layer.
[header_data appendFormat:@"req.setRequestHeader(%@, %@);",
EscapeAndQuoteToNSString(it.name()),
EscapeAndQuoteToNSString(it.value())];
}
NSString* html_string =
[NSString stringWithFormat:kPostRequestTemplate,
EscapeAndQuoteToNSString(url.spec()),
header_data, EscapeAndQuoteToNSString(body)];
// |url| is used as the baseURL to avoid CORS issues.
[web_view loadHTMLString:html_string baseURL:net::NSURLWithGURL(url)];
}
} // namespace
#pragma mark - GaiaAuthFetcherNavigationDelegate
@implementation GaiaAuthFetcherNavigationDelegate {
GaiaAuthFetcherIOSBridge* bridge_; // weak
}
- (instancetype)initWithBridge:(GaiaAuthFetcherIOSBridge*)bridge {
self = [super init];
if (self) {
bridge_ = bridge;
}
return self;
}
#pragma mark WKNavigationDelegate
- (void)webView:(WKWebView*)webView
didFailNavigation:(WKNavigation*)navigation
withError:(NSError*)error {
DVLOG(1) << "Gaia fetcher navigation failed: "
<< base::SysNSStringToUTF8(error.localizedDescription);
bridge_->URLFetchFailure(false /* is_cancelled */);
}
- (void)webView:(WKWebView*)webView
didFailProvisionalNavigation:(WKNavigation*)navigation
withError:(NSError*)error {
DVLOG(1) << "Gaia fetcher provisional navigation failed: "
<< base::SysNSStringToUTF8(error.localizedDescription);
bridge_->URLFetchFailure(false /* is_cancelled */);
}
- (void)webView:(WKWebView*)webView
didFinishNavigation:(WKNavigation*)navigation {
// A WKNavigation is an opaque object. The only way to access the body of the
// response is via Javascript.
DVLOG(2) << "WKWebView loaded:" << net::GURLWithNSURL(webView.URL);
[webView evaluateJavaScript:kReadResponseTemplate
completionHandler:^(NSString* result, NSError* error) {
if (error || !result) {
DVLOG(1) << "Gaia fetcher extract body failed:"
<< base::SysNSStringToUTF8(error.localizedDescription);
bridge_->URLFetchFailure(false /* is_cancelled */);
} else {
DCHECK([result isKindOfClass:[NSString class]]);
bridge_->URLFetchSuccess(base::SysNSStringToUTF8(result));
}
}];
}
@end
#pragma mark - GaiaAuthFetcherIOSBridge::Request
GaiaAuthFetcherIOSBridge::Request::Request()
: pending(false), url(), headers(), body() {}
GaiaAuthFetcherIOSBridge::Request::Request(const GURL& request_url,
const std::string& request_headers,
const std::string& request_body)
: pending(true),
url(request_url),
headers(request_headers),
body(request_body) {}
#pragma mark - GaiaAuthFetcherIOSBridge
GaiaAuthFetcherIOSBridge::GaiaAuthFetcherIOSBridge(
GaiaAuthFetcherIOS* fetcher,
web::BrowserState* browser_state)
: browser_state_(browser_state), fetcher_(fetcher), request_() {
ActiveStateManager::FromBrowserState(browser_state_)->AddObserver(this);
}
GaiaAuthFetcherIOSBridge::~GaiaAuthFetcherIOSBridge() {
ActiveStateManager::FromBrowserState(browser_state_)->RemoveObserver(this);
ResetWKWebView();
}
void GaiaAuthFetcherIOSBridge::Fetch(const GURL& url,
const std::string& headers,
const std::string& body) {
request_ = Request(url, headers, body);
FetchPendingRequest();
}
void GaiaAuthFetcherIOSBridge::Cancel() {
if (!request_.pending) {
return;
}
[GetWKWebView() stopLoading];
URLFetchFailure(true /* is_cancelled */);
}
void GaiaAuthFetcherIOSBridge::URLFetchSuccess(const std::string& data) {
if (!request_.pending) {
return;
}
GURL url = FinishPendingRequest();
// WKWebViewNavigationDelegate API doesn't give any way to get the HTTP
// response code of a navigation. Default to 200 for success.
fetcher_->FetchComplete(url, data, net::ResponseCookies(),
net::URLRequestStatus(), 200);
}
void GaiaAuthFetcherIOSBridge::URLFetchFailure(bool is_cancelled) {
if (!request_.pending) {
return;
}
GURL url = FinishPendingRequest();
// WKWebViewNavigationDelegate API doesn't give any way to get the HTTP
// response code of a navigation. Default to 500 for error.
int error = is_cancelled ? net::ERR_ABORTED : net::ERR_FAILED;
fetcher_->FetchComplete(url, std::string(), net::ResponseCookies(),
net::URLRequestStatus::FromError(error), 500);
}
void GaiaAuthFetcherIOSBridge::FetchPendingRequest() {
if (!request_.pending)
return;
if (!request_.body.empty() && !base::ios::IsRunningOnIOS11OrLater()) {
DoPostRequest(GetWKWebView(), request_.body, request_.headers,
request_.url);
} else {
[GetWKWebView()
loadRequest:GetRequest(request_.body, request_.headers, request_.url)];
}
}
GURL GaiaAuthFetcherIOSBridge::FinishPendingRequest() {
GURL url = request_.url;
request_ = Request();
return url;
}
WKWebView* GaiaAuthFetcherIOSBridge::GetWKWebView() {
if (!ActiveStateManager::FromBrowserState(browser_state_)->IsActive()) {
// |browser_state_| is not active, WKWebView linked to this browser state
// should not exist or be created.
return nil;
}
if (!web_view_) {
web_view_ = BuildWKWebView();
navigation_delegate_ =
[[GaiaAuthFetcherNavigationDelegate alloc] initWithBridge:this];
[web_view_ setNavigationDelegate:navigation_delegate_];
}
return web_view_;
}
void GaiaAuthFetcherIOSBridge::ResetWKWebView() {
[web_view_ setNavigationDelegate:nil];
[web_view_ stopLoading];
web_view_ = nil;
navigation_delegate_ = nil;
}
WKWebView* GaiaAuthFetcherIOSBridge::BuildWKWebView() {
return web::BuildWKWebView(CGRectZero, browser_state_);
}
void GaiaAuthFetcherIOSBridge::OnActive() {
// |browser_state_| is now active. If there is a pending request, restart it.
FetchPendingRequest();
}
void GaiaAuthFetcherIOSBridge::OnInactive() {
// |browser_state_| is now inactive. Stop using |web_view_| and don't create
// a new one until it is active.
ResetWKWebView();
}
#pragma mark - GaiaAuthFetcherIOS definition
GaiaAuthFetcherIOS::GaiaAuthFetcherIOS(
GaiaAuthConsumer* consumer,
const std::string& source,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
web::BrowserState* browser_state)
: GaiaAuthFetcher(consumer, source, url_loader_factory),
bridge_(new GaiaAuthFetcherIOSBridge(this, browser_state)),
browser_state_(browser_state) {}
GaiaAuthFetcherIOS::~GaiaAuthFetcherIOS() {
}
void GaiaAuthFetcherIOS::CreateAndStartGaiaFetcher(
const std::string& body,
const std::string& headers,
const GURL& gaia_gurl,
int load_flags,
const net::NetworkTrafficAnnotationTag& traffic_annotation) {
DCHECK(!HasPendingFetch()) << "Tried to fetch two things at once!";
bool cookies_required = !(load_flags & (net::LOAD_DO_NOT_SEND_COOKIES |
net::LOAD_DO_NOT_SAVE_COOKIES));
if (!ShouldUseGaiaAuthFetcherIOS() || !cookies_required) {
GaiaAuthFetcher::CreateAndStartGaiaFetcher(body, headers, gaia_gurl,
load_flags, traffic_annotation);
return;
}
DVLOG(2) << "Gaia fetcher URL: " << gaia_gurl.spec();
DVLOG(2) << "Gaia fetcher headers: " << headers;
DVLOG(2) << "Gaia fetcher body: " << body;
// The fetch requires cookies and WKWebView is being used. The only way to do
// a network request with cookies sent and saved is by making it through a
// WKWebView.
SetPendingFetch(true);
bridge_->Fetch(gaia_gurl, headers, body);
}
void GaiaAuthFetcherIOS::CancelRequest() {
if (!HasPendingFetch()) {
return;
}
bridge_->Cancel();
GaiaAuthFetcher::CancelRequest();
}
void GaiaAuthFetcherIOS::FetchComplete(const GURL& url,
const std::string& data,
const net::ResponseCookies& cookies,
const net::URLRequestStatus& status,
int response_code) {
DVLOG(2) << "Response " << url.spec() << ", code = " << response_code << "\n";
DVLOG(2) << "data: " << data << "\n";
SetPendingFetch(false);
DispatchFetchedRequest(url, data, cookies,
static_cast<net::Error>(status.error()),
response_code);
}
void GaiaAuthFetcherIOS::SetShouldUseGaiaAuthFetcherIOSForTesting(
bool use_gaia_fetcher_ios) {
g_should_use_gaia_auth_fetcher_ios = use_gaia_fetcher_ios;
}
bool GaiaAuthFetcherIOS::ShouldUseGaiaAuthFetcherIOS() {
return g_should_use_gaia_auth_fetcher_ios;
}