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. 4 5#include "components/signin/ios/browser/account_consistency_service.h" 6 7#import <WebKit/WebKit.h> 8 9#include "base/bind.h" 10#include "base/logging.h" 11#import "base/mac/foundation_util.h" 12#include "base/macros.h" 13#include "base/strings/sys_string_conversions.h" 14#include "components/content_settings/core/browser/cookie_settings.h" 15#include "components/google/core/common/google_util.h" 16#include "components/prefs/pref_registry_simple.h" 17#include "components/prefs/pref_service.h" 18#include "components/prefs/scoped_user_pref_update.h" 19#include "components/signin/core/browser/account_reconcilor.h" 20#include "components/signin/core/browser/signin_header_helper.h" 21#include "components/signin/public/base/account_consistency_method.h" 22#include "components/signin/public/identity_manager/accounts_cookie_mutator.h" 23#include "ios/web/common/web_view_creation_util.h" 24#include "ios/web/public/browser_state.h" 25#import "ios/web/public/navigation/web_state_policy_decider.h" 26#include "net/base/mac/url_conversions.h" 27#include "net/base/registry_controlled_domains/registry_controlled_domain.h" 28#include "url/gurl.h" 29 30#if !defined(__has_feature) || !__has_feature(objc_arc) 31#error "This file requires ARC support." 32#endif 33 34namespace { 35 36// Threshold (in hours) used to control whether the CHROME_CONNECTED cookie 37// should be added again on a domain it was previously set. 38const int kHoursThresholdToReAddCookie = 24; 39 40// JavaScript template used to set (or delete) the CHROME_CONNECTED cookie. 41// It takes 3 arguments: the domain of the cookie, its value and its expiration 42// date. It also clears the legacy X-CHROME-CONNECTED cookie. 43NSString* const kChromeConnectedCookieTemplate = 44 @"<html><script>domain=\"%@\";" 45 "document.cookie=\"X-CHROME-CONNECTED=; path=/; domain=\" + domain + \";" 46 " expires=Thu, 01-Jan-1970 00:00:01 GMT\";" 47 "document.cookie=\"CHROME_CONNECTED=%@; path=/; domain=\" + domain + \";" 48 " expires=\" + new Date(%f).toGMTString() + \"; secure;" 49 " samesite=lax;\"</script></html>"; 50 51// WebStatePolicyDecider that monitors the HTTP headers on Gaia responses, 52// reacting on the X-Chrome-Manage-Accounts header and notifying its delegate. 53// It also notifies the AccountConsistencyService of domains it should add the 54// CHROME_CONNECTED cookie to. 55class AccountConsistencyHandler : public web::WebStatePolicyDecider { 56 public: 57 AccountConsistencyHandler(web::WebState* web_state, 58 AccountConsistencyService* service, 59 AccountReconcilor* account_reconcilor, 60 id<ManageAccountsDelegate> delegate); 61 62 private: 63 // web::WebStatePolicyDecider override 64 bool ShouldAllowResponse(NSURLResponse* response, 65 bool for_main_frame) override; 66 void WebStateDestroyed() override; 67 68 AccountConsistencyService* account_consistency_service_; // Weak. 69 AccountReconcilor* account_reconcilor_; // Weak. 70 __weak id<ManageAccountsDelegate> delegate_; 71}; 72} 73 74AccountConsistencyHandler::AccountConsistencyHandler( 75 web::WebState* web_state, 76 AccountConsistencyService* service, 77 AccountReconcilor* account_reconcilor, 78 id<ManageAccountsDelegate> delegate) 79 : web::WebStatePolicyDecider(web_state), 80 account_consistency_service_(service), 81 account_reconcilor_(account_reconcilor), 82 delegate_(delegate) {} 83 84bool AccountConsistencyHandler::ShouldAllowResponse(NSURLResponse* response, 85 bool for_main_frame) { 86 NSHTTPURLResponse* http_response = 87 base::mac::ObjCCast<NSHTTPURLResponse>(response); 88 if (!http_response) 89 return true; 90 91 GURL url = net::GURLWithNSURL(http_response.URL); 92 if (google_util::IsGoogleDomainUrl( 93 url, google_util::ALLOW_SUBDOMAIN, 94 google_util::DISALLOW_NON_STANDARD_PORTS)) { 95 // User is showing intent to navigate to a Google domain. Add the 96 // CHROME_CONNECTED cookie to the domain if necessary. 97 std::string domain = net::registry_controlled_domains::GetDomainAndRegistry( 98 url, net::registry_controlled_domains::EXCLUDE_PRIVATE_REGISTRIES); 99 account_consistency_service_->AddChromeConnectedCookieToDomain( 100 domain, true /* force_update_if_too_old */); 101 account_consistency_service_->AddChromeConnectedCookieToDomain( 102 "google.com", true /* force_update_if_too_old */); 103 } 104 105 if (!gaia::IsGaiaSignonRealm(url.GetOrigin())) 106 return true; 107 NSString* manage_accounts_header = [[http_response allHeaderFields] 108 objectForKey:@"X-Chrome-Manage-Accounts"]; 109 if (!manage_accounts_header) 110 return true; 111 112 signin::ManageAccountsParams params = signin::BuildManageAccountsParams( 113 base::SysNSStringToUTF8(manage_accounts_header)); 114 115 account_reconcilor_->OnReceivedManageAccountsResponse(params.service_type); 116 switch (params.service_type) { 117 case signin::GAIA_SERVICE_TYPE_INCOGNITO: { 118 GURL continue_url = GURL(params.continue_url); 119 DLOG_IF(ERROR, !params.continue_url.empty() && !continue_url.is_valid()) 120 << "Invalid continuation URL: \"" << continue_url << "\""; 121 [delegate_ onGoIncognito:continue_url]; 122 break; 123 } 124 case signin::GAIA_SERVICE_TYPE_SIGNUP: 125 case signin::GAIA_SERVICE_TYPE_ADDSESSION: 126 [delegate_ onAddAccount]; 127 break; 128 case signin::GAIA_SERVICE_TYPE_SIGNOUT: 129 case signin::GAIA_SERVICE_TYPE_DEFAULT: 130 [delegate_ onManageAccounts]; 131 break; 132 case signin::GAIA_SERVICE_TYPE_NONE: 133 NOTREACHED(); 134 break; 135 } 136 137 // WKWebView loads a blank page even if the response code is 204 138 // ("No Content"). http://crbug.com/368717 139 // 140 // Manage accounts responses are handled via native UI. Abort this request 141 // for the following reasons: 142 // * Avoid loading a blank page in WKWebView. 143 // * Avoid adding this request to history. 144 return false; 145} 146 147void AccountConsistencyHandler::WebStateDestroyed() { 148 account_consistency_service_->RemoveWebStateHandler(web_state()); 149} 150 151// WKWebView navigation delegate that calls its callback every time a navigation 152// has finished. 153@interface AccountConsistencyNavigationDelegate : NSObject<WKNavigationDelegate> 154 155// Designated initializer. |callback| will be called every time a navigation has 156// finished. |callback| must not be empty. 157- (instancetype)initWithCallback:(const base::RepeatingClosure&)callback 158 NS_DESIGNATED_INITIALIZER; 159 160- (instancetype)init NS_UNAVAILABLE; 161@end 162 163@implementation AccountConsistencyNavigationDelegate { 164 // Callback that will be called every time a navigation has finished. 165 base::RepeatingClosure _callback; 166} 167 168- (instancetype)initWithCallback:(const base::RepeatingClosure&)callback { 169 self = [super init]; 170 if (self) { 171 DCHECK(!callback.is_null()); 172 _callback = callback; 173 } 174 return self; 175} 176 177- (instancetype)init { 178 NOTREACHED(); 179 return nil; 180} 181 182#pragma mark - WKNavigationDelegate 183 184- (void)webView:(WKWebView*)webView 185 didFinishNavigation:(WKNavigation*)navigation { 186 _callback.Run(); 187} 188 189@end 190 191const char AccountConsistencyService::kDomainsWithCookiePref[] = 192 "signin.domains_with_cookie"; 193 194AccountConsistencyService::CookieRequest 195AccountConsistencyService::CookieRequest::CreateAddCookieRequest( 196 const std::string& domain) { 197 AccountConsistencyService::CookieRequest cookie_request; 198 cookie_request.request_type = ADD_CHROME_CONNECTED_COOKIE; 199 cookie_request.domain = domain; 200 return cookie_request; 201} 202 203AccountConsistencyService::CookieRequest 204AccountConsistencyService::CookieRequest::CreateRemoveCookieRequest( 205 const std::string& domain, 206 base::OnceClosure callback) { 207 AccountConsistencyService::CookieRequest cookie_request; 208 cookie_request.request_type = REMOVE_CHROME_CONNECTED_COOKIE; 209 cookie_request.domain = domain; 210 cookie_request.callback = std::move(callback); 211 return cookie_request; 212} 213 214AccountConsistencyService::CookieRequest::CookieRequest() = default; 215 216AccountConsistencyService::CookieRequest::~CookieRequest() = default; 217 218AccountConsistencyService::CookieRequest::CookieRequest( 219 AccountConsistencyService::CookieRequest&&) = default; 220 221AccountConsistencyService::AccountConsistencyService( 222 web::BrowserState* browser_state, 223 PrefService* prefs, 224 AccountReconcilor* account_reconcilor, 225 scoped_refptr<content_settings::CookieSettings> cookie_settings, 226 signin::IdentityManager* identity_manager) 227 : browser_state_(browser_state), 228 prefs_(prefs), 229 account_reconcilor_(account_reconcilor), 230 cookie_settings_(cookie_settings), 231 identity_manager_(identity_manager), 232 applying_cookie_requests_(false) { 233 identity_manager_->AddObserver(this); 234 ActiveStateManager::FromBrowserState(browser_state_)->AddObserver(this); 235 LoadFromPrefs(); 236 if (identity_manager_->HasPrimaryAccount()) { 237 AddChromeConnectedCookies(); 238 } else { 239 RemoveChromeConnectedCookies(base::OnceClosure()); 240 } 241} 242 243AccountConsistencyService::~AccountConsistencyService() { 244 DCHECK(!web_view_); 245 DCHECK(!navigation_delegate_); 246} 247 248// static 249void AccountConsistencyService::RegisterPrefs(PrefRegistrySimple* registry) { 250 registry->RegisterDictionaryPref( 251 AccountConsistencyService::kDomainsWithCookiePref); 252} 253 254void AccountConsistencyService::SetWebStateHandler( 255 web::WebState* web_state, 256 id<ManageAccountsDelegate> delegate) { 257 DCHECK_EQ(0u, web_state_handlers_.count(web_state)); 258 web_state_handlers_[web_state].reset(new AccountConsistencyHandler( 259 web_state, this, account_reconcilor_, delegate)); 260} 261 262void AccountConsistencyService::RemoveWebStateHandler( 263 web::WebState* web_state) { 264 DCHECK_LT(0u, web_state_handlers_.count(web_state)); 265 web_state_handlers_.erase(web_state); 266} 267 268bool AccountConsistencyService::ShouldAddChromeConnectedCookieToDomain( 269 const std::string& domain, 270 bool force_update_if_too_old) { 271 auto it = last_cookie_update_map_.find(domain); 272 if (it == last_cookie_update_map_.end()) { 273 // |domain| isn't in the map, always add the cookie. 274 return true; 275 } 276 if (!force_update_if_too_old) { 277 // |domain| is in the map and the cookie is considered valid. Don't add it 278 // again. 279 return false; 280 } 281 return (base::Time::Now() - it->second) > 282 base::TimeDelta::FromHours(kHoursThresholdToReAddCookie); 283} 284 285void AccountConsistencyService::RemoveChromeConnectedCookies( 286 base::OnceClosure callback) { 287 DCHECK(!browser_state_->IsOffTheRecord()); 288 if (last_cookie_update_map_.empty()) { 289 if (!callback.is_null()) 290 std::move(callback).Run(); 291 return; 292 } 293 std::map<std::string, base::Time> last_cookie_update_map = 294 last_cookie_update_map_; 295 auto iter_last_item = std::prev(last_cookie_update_map.end()); 296 for (auto iter = last_cookie_update_map.begin(); iter != iter_last_item; 297 iter++) { 298 RemoveChromeConnectedCookieFromDomain(iter->first, base::OnceClosure()); 299 } 300 RemoveChromeConnectedCookieFromDomain(iter_last_item->first, 301 std::move(callback)); 302} 303 304void AccountConsistencyService::AddChromeConnectedCookieToDomain( 305 const std::string& domain, 306 bool force_update_if_too_old) { 307 if (!ShouldAddChromeConnectedCookieToDomain(domain, 308 force_update_if_too_old)) { 309 return; 310 } 311 last_cookie_update_map_[domain] = base::Time::Now(); 312 cookie_requests_.push_back(CookieRequest::CreateAddCookieRequest(domain)); 313 ApplyCookieRequests(); 314} 315 316void AccountConsistencyService::RemoveChromeConnectedCookieFromDomain( 317 const std::string& domain, 318 base::OnceClosure callback) { 319 DCHECK_NE(0ul, last_cookie_update_map_.count(domain)); 320 last_cookie_update_map_.erase(domain); 321 cookie_requests_.push_back( 322 CookieRequest::CreateRemoveCookieRequest(domain, std::move(callback))); 323 ApplyCookieRequests(); 324} 325 326void AccountConsistencyService::LoadFromPrefs() { 327 const base::DictionaryValue* dict = 328 prefs_->GetDictionary(kDomainsWithCookiePref); 329 for (base::DictionaryValue::Iterator it(*dict); !it.IsAtEnd(); it.Advance()) { 330 last_cookie_update_map_[it.key()] = base::Time(); 331 } 332} 333 334void AccountConsistencyService::Shutdown() { 335 identity_manager_->RemoveObserver(this); 336 ActiveStateManager::FromBrowserState(browser_state_)->RemoveObserver(this); 337 ResetWKWebView(); 338 web_state_handlers_.clear(); 339} 340 341void AccountConsistencyService::ApplyCookieRequests() { 342 if (applying_cookie_requests_) { 343 // A cookie request is already being applied, the following ones will be 344 // handled as soon as the current one is done. 345 return; 346 } 347 if (cookie_requests_.empty()) { 348 return; 349 } 350 if (!ActiveStateManager::FromBrowserState(browser_state_)->IsActive()) { 351 // Web view usage isn't active for now, ignore cookie requests for now and 352 // wait to be notified that it became active again. 353 return; 354 } 355 applying_cookie_requests_ = true; 356 357 const GURL url("https://" + cookie_requests_.front().domain); 358 std::string cookie_value = ""; 359 // Expiration date of the cookie in the JavaScript convention of time, a 360 // number of milliseconds since the epoch. 361 double expiration_date = 0; 362 switch (cookie_requests_.front().request_type) { 363 case ADD_CHROME_CONNECTED_COOKIE: 364 cookie_value = signin::BuildMirrorRequestCookieIfPossible( 365 url, identity_manager_->GetPrimaryAccountInfo().gaia, 366 signin::AccountConsistencyMethod::kMirror, cookie_settings_.get(), 367 signin::PROFILE_MODE_DEFAULT); 368 if (cookie_value.empty()) { 369 // Don't add the cookie. Tentatively correct |last_cookie_update_map_|. 370 last_cookie_update_map_.erase(cookie_requests_.front().domain); 371 FinishedApplyingCookieRequest(false); 372 return; 373 } 374 // Create expiration date of Now+2y to roughly follow the SAPISID cookie. 375 expiration_date = 376 (base::Time::Now() + base::TimeDelta::FromDays(730)).ToJsTime(); 377 break; 378 case REMOVE_CHROME_CONNECTED_COOKIE: 379 // Nothing to do. Default values correspond to removing the cookie (no 380 // value, expiration date in the past). 381 break; 382 } 383 NSString* html = [NSString 384 stringWithFormat:kChromeConnectedCookieTemplate, 385 base::SysUTF8ToNSString(url.host()), 386 base::SysUTF8ToNSString(cookie_value), expiration_date]; 387 // Load an HTML string with embedded JavaScript that will set or remove the 388 // cookie. By setting the base URL to |url|, this effectively allows to modify 389 // cookies on the correct domain without having to do a network request. 390 [GetWKWebView() loadHTMLString:html baseURL:net::NSURLWithGURL(url)]; 391} 392 393void AccountConsistencyService::FinishedApplyingCookieRequest(bool success) { 394 DCHECK(!cookie_requests_.empty()); 395 CookieRequest& request = cookie_requests_.front(); 396 if (success) { 397 DictionaryPrefUpdate update( 398 prefs_, AccountConsistencyService::kDomainsWithCookiePref); 399 switch (request.request_type) { 400 case ADD_CHROME_CONNECTED_COOKIE: 401 // Add request.domain to prefs, use |true| as a dummy value (that is 402 // never used), as the dictionary is used as a set. 403 update->SetKey(request.domain, base::Value(true)); 404 break; 405 case REMOVE_CHROME_CONNECTED_COOKIE: 406 // Remove request.domain from prefs. 407 update->RemoveWithoutPathExpansion(request.domain, nullptr); 408 break; 409 } 410 } 411 base::OnceClosure callback(std::move(request.callback)); 412 cookie_requests_.pop_front(); 413 applying_cookie_requests_ = false; 414 ApplyCookieRequests(); 415 if (!callback.is_null()) { 416 std::move(callback).Run(); 417 } 418} 419 420WKWebView* AccountConsistencyService::GetWKWebView() { 421 if (!ActiveStateManager::FromBrowserState(browser_state_)->IsActive()) { 422 // |browser_state_| is not active, WKWebView linked to this browser state 423 // should not exist or be created. 424 return nil; 425 } 426 if (!web_view_) { 427 web_view_ = BuildWKWebView(); 428 navigation_delegate_ = [[AccountConsistencyNavigationDelegate alloc] 429 initWithCallback:base::BindRepeating(&AccountConsistencyService:: 430 FinishedApplyingCookieRequest, 431 base::Unretained(this), true)]; 432 [web_view_ setNavigationDelegate:navigation_delegate_]; 433 } 434 return web_view_; 435} 436 437WKWebView* AccountConsistencyService::BuildWKWebView() { 438 return web::BuildWKWebView(CGRectZero, browser_state_); 439} 440 441void AccountConsistencyService::ResetWKWebView() { 442 [web_view_ setNavigationDelegate:nil]; 443 [web_view_ stopLoading]; 444 web_view_ = nil; 445 navigation_delegate_ = nil; 446 applying_cookie_requests_ = false; 447} 448 449void AccountConsistencyService::AddChromeConnectedCookies() { 450 DCHECK(!browser_state_->IsOffTheRecord()); 451 // These cookie request are preventive and not a strong signal (unlike 452 // navigation to a domain). Don't force update the old cookies in this case. 453 AddChromeConnectedCookieToDomain("google.com", 454 false /* force_update_if_too_old */); 455 AddChromeConnectedCookieToDomain("youtube.com", 456 false /* force_update_if_too_old */); 457} 458 459void AccountConsistencyService::OnBrowsingDataRemoved() { 460 // CHROME_CONNECTED cookies have been removed, update internal state 461 // accordingly. 462 ResetWKWebView(); 463 for (auto& cookie_request : cookie_requests_) { 464 base::OnceClosure callback(std::move(cookie_request.callback)); 465 if (!callback.is_null()) { 466 std::move(callback).Run(); 467 } 468 } 469 cookie_requests_.clear(); 470 last_cookie_update_map_.clear(); 471 base::DictionaryValue dict; 472 prefs_->Set(kDomainsWithCookiePref, dict); 473 474 // SAPISID cookie has been removed, notify the GCMS. 475 // TODO(https://crbug.com/930582) : Remove the need to expose this method 476 // or move it to the network::CookieManager. 477 identity_manager_->GetAccountsCookieMutator()->ForceTriggerOnCookieChange(); 478} 479 480void AccountConsistencyService::OnPrimaryAccountSet( 481 const CoreAccountInfo& account_info) { 482 AddChromeConnectedCookies(); 483} 484 485void AccountConsistencyService::OnPrimaryAccountCleared( 486 const CoreAccountInfo& previous_account_info) { 487 // There is not need to remove CHROME_CONNECTED cookies on |GoogleSignedOut| 488 // events as these cookies will be removed by the GaiaCookieManagerServer 489 // right before fetching the Gaia logout request. 490} 491 492void AccountConsistencyService::OnAccountsInCookieUpdated( 493 const signin::AccountsInCookieJarInfo& accounts_in_cookie_jar_info, 494 const GoogleServiceAuthError& error) { 495 AddChromeConnectedCookies(); 496} 497 498void AccountConsistencyService::OnActive() { 499 // |browser_state_| is now active. There might be some pending cookie requests 500 // to apply. 501 ApplyCookieRequests(); 502} 503 504void AccountConsistencyService::OnInactive() { 505 // |browser_state_| is now inactive. Stop using |web_view_| and don't create 506 // a new one until it is active. 507 ResetWKWebView(); 508} 509