| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 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. | |
| 4 | |
| 5 #include "chrome/browser/extensions/app_notify_channel_setup.h" | |
| 6 | |
| 7 #include <string> | |
| 8 #include <vector> | |
| 9 | |
| 10 #include "base/basictypes.h" | |
| 11 #include "base/bind.h" | |
| 12 #include "base/command_line.h" | |
| 13 #include "base/json/json_reader.h" | |
| 14 #include "base/message_loop.h" | |
| 15 #include "base/metrics/histogram.h" | |
| 16 #include "base/prefs/pref_service.h" | |
| 17 #include "base/stringprintf.h" | |
| 18 #include "chrome/browser/profiles/profile.h" | |
| 19 #include "chrome/browser/signin/signin_manager.h" | |
| 20 #include "chrome/browser/signin/signin_manager_factory.h" | |
| 21 #include "chrome/browser/signin/token_service.h" | |
| 22 #include "chrome/browser/signin/token_service_factory.h" | |
| 23 #include "chrome/common/chrome_switches.h" | |
| 24 #include "chrome/common/pref_names.h" | |
| 25 #include "content/public/browser/browser_thread.h" | |
| 26 #include "google_apis/gaia/gaia_constants.h" | |
| 27 #include "google_apis/gaia/gaia_urls.h" | |
| 28 #include "net/base/escape.h" | |
| 29 #include "net/base/load_flags.h" | |
| 30 #include "net/http/http_request_headers.h" | |
| 31 #include "net/http/http_status_code.h" | |
| 32 #include "net/url_request/url_fetcher.h" | |
| 33 #include "net/url_request/url_request_status.h" | |
| 34 | |
| 35 using base::StringPrintf; | |
| 36 using content::BrowserThread; | |
| 37 using net::URLFetcher; | |
| 38 | |
| 39 namespace extensions { | |
| 40 | |
| 41 namespace { | |
| 42 | |
| 43 static const char kChannelSetupAuthError[] = "unauthorized"; | |
| 44 static const char kChannelSetupInternalError[] = "internal_error"; | |
| 45 static const char kChannelSetupCanceledByUser[] = "canceled_by_user"; | |
| 46 static const char kAuthorizationHeaderFormat[] = | |
| 47 "Authorization: Bearer %s"; | |
| 48 static const char kOAuth2IssueTokenURL[] = | |
| 49 "https://www.googleapis.com/oauth2/v2/IssueToken"; | |
| 50 static const char kOAuth2IssueTokenBodyFormat[] = | |
| 51 "force=true" | |
| 52 "&response_type=token" | |
| 53 "&scope=%s" | |
| 54 "&client_id=%s" | |
| 55 "&origin=%s"; | |
| 56 static const char kOAuth2IssueTokenScope[] = | |
| 57 "https://www.googleapis.com/auth/chromewebstore.notification"; | |
| 58 static const char kCWSChannelServiceURL[] = | |
| 59 "https://www.googleapis.com/chromewebstore/v1.1/channels/id"; | |
| 60 | |
| 61 static AppNotifyChannelSetup::InterceptorForTests* | |
| 62 g_interceptor_for_tests = NULL; | |
| 63 | |
| 64 } // namespace. | |
| 65 | |
| 66 // static | |
| 67 void AppNotifyChannelSetup::SetInterceptorForTests( | |
| 68 AppNotifyChannelSetup::InterceptorForTests* interceptor) { | |
| 69 // Only one interceptor at a time, please. | |
| 70 CHECK(g_interceptor_for_tests == NULL); | |
| 71 g_interceptor_for_tests = interceptor; | |
| 72 } | |
| 73 | |
| 74 AppNotifyChannelSetup::AppNotifyChannelSetup( | |
| 75 Profile* profile, | |
| 76 const std::string& extension_id, | |
| 77 const std::string& client_id, | |
| 78 const GURL& requestor_url, | |
| 79 int return_route_id, | |
| 80 int callback_id, | |
| 81 AppNotifyChannelUI* ui, | |
| 82 base::WeakPtr<AppNotifyChannelSetup::Delegate> delegate) | |
| 83 : profile_(profile->GetOriginalProfile()), | |
| 84 extension_id_(extension_id), | |
| 85 client_id_(client_id), | |
| 86 requestor_url_(requestor_url), | |
| 87 return_route_id_(return_route_id), | |
| 88 callback_id_(callback_id), | |
| 89 delegate_(delegate), | |
| 90 ui_(ui), | |
| 91 state_(INITIAL), | |
| 92 oauth2_access_token_failure_(false) {} | |
| 93 | |
| 94 AppNotifyChannelSetup::~AppNotifyChannelSetup() {} | |
| 95 | |
| 96 void AppNotifyChannelSetup::Start() { | |
| 97 AddRef(); // Balanced in ReportResult. | |
| 98 | |
| 99 if (g_interceptor_for_tests) { | |
| 100 std::string channel_id; | |
| 101 SetupError error; | |
| 102 g_interceptor_for_tests->DoIntercept(this, &channel_id, &error); | |
| 103 state_ = error == NONE ? CHANNEL_ID_SETUP_DONE : ERROR_STATE; | |
| 104 | |
| 105 // Use PostTask so the message loop runs before notifying the delegate, like | |
| 106 // in the real implementation. | |
| 107 MessageLoop::current()->PostTask( | |
| 108 FROM_HERE, | |
| 109 base::Bind(&AppNotifyChannelSetup::ReportResult, | |
| 110 base::Unretained(this), channel_id, error)); | |
| 111 return; | |
| 112 } | |
| 113 | |
| 114 BeginLogin(); | |
| 115 } | |
| 116 | |
| 117 void AppNotifyChannelSetup::OnGetTokenSuccess( | |
| 118 const std::string& access_token, | |
| 119 const base::Time& expiration_time) { | |
| 120 oauth2_access_token_ = access_token; | |
| 121 EndGetAccessToken(true); | |
| 122 } | |
| 123 void AppNotifyChannelSetup::OnGetTokenFailure( | |
| 124 const GoogleServiceAuthError& error) { | |
| 125 EndGetAccessToken(false); | |
| 126 } | |
| 127 | |
| 128 void AppNotifyChannelSetup::OnSyncSetupResult(bool enabled) { | |
| 129 EndLogin(enabled); | |
| 130 } | |
| 131 | |
| 132 void AppNotifyChannelSetup::OnURLFetchComplete(const net::URLFetcher* source) { | |
| 133 CHECK(source); | |
| 134 switch (state_) { | |
| 135 case RECORD_GRANT_STARTED: | |
| 136 EndRecordGrant(source); | |
| 137 break; | |
| 138 case CHANNEL_ID_SETUP_STARTED: | |
| 139 EndGetChannelId(source); | |
| 140 break; | |
| 141 default: | |
| 142 CHECK(false) << "Wrong state: " << state_; | |
| 143 break; | |
| 144 } | |
| 145 } | |
| 146 | |
| 147 // The contents of |body| should be URL-encoded as appropriate. | |
| 148 URLFetcher* AppNotifyChannelSetup::CreateURLFetcher( | |
| 149 const GURL& url, const std::string& body, const std::string& auth_token) { | |
| 150 CHECK(url.is_valid()); | |
| 151 URLFetcher::RequestType type = | |
| 152 body.empty() ? URLFetcher::GET : URLFetcher::POST; | |
| 153 URLFetcher* fetcher = net::URLFetcher::Create(0, url, type, this); | |
| 154 fetcher->SetRequestContext(profile_->GetRequestContext()); | |
| 155 // Always set flags to neither send nor save cookies. | |
| 156 fetcher->SetLoadFlags( | |
| 157 net::LOAD_DO_NOT_SEND_COOKIES | net::LOAD_DO_NOT_SAVE_COOKIES); | |
| 158 fetcher->SetExtraRequestHeaders(MakeAuthorizationHeader(auth_token)); | |
| 159 if (!body.empty()) { | |
| 160 fetcher->SetUploadData("application/x-www-form-urlencoded", body); | |
| 161 } | |
| 162 return fetcher; | |
| 163 } | |
| 164 | |
| 165 bool AppNotifyChannelSetup::ShouldPromptForLogin() const { | |
| 166 std::string username = | |
| 167 SigninManagerFactory::GetForProfile(profile_)->GetAuthenticatedUsername(); | |
| 168 // Prompt for login if either: | |
| 169 // 1. the user has not logged in at all or | |
| 170 // 2. if the user is logged in but there is no OAuth2 login token. | |
| 171 // The latter happens for users who are already logged in before the | |
| 172 // code to generate OAuth2 login token is released. | |
| 173 // 3. If the OAuth2 login token does not work anymore. | |
| 174 // This can happen if the user explicitly revoked access to Google Chrome | |
| 175 // from Google Accounts page. | |
| 176 return username.empty() || | |
| 177 !TokenServiceFactory::GetForProfile(profile_)->HasOAuthLoginToken() || | |
| 178 oauth2_access_token_failure_; | |
| 179 } | |
| 180 | |
| 181 namespace { | |
| 182 | |
| 183 enum LoginNeededHistogram { | |
| 184 LOGIN_NEEDED, | |
| 185 LOGIN_NOT_NEEDED, | |
| 186 LOGIN_NEEDED_BOUNDARY | |
| 187 }; | |
| 188 | |
| 189 enum LoginSuccessHistogram { | |
| 190 LOGIN_SUCCESS, | |
| 191 LOGIN_FAILURE, | |
| 192 LOGIN_SUCCESS_BOUNDARY | |
| 193 }; | |
| 194 | |
| 195 } // namespace | |
| 196 | |
| 197 void AppNotifyChannelSetup::BeginLogin() { | |
| 198 CHECK_EQ(INITIAL, state_); | |
| 199 state_ = LOGIN_STARTED; | |
| 200 bool login_needed = ShouldPromptForLogin(); | |
| 201 UMA_HISTOGRAM_ENUMERATION("AppNotify.ChannelSetupBegin", | |
| 202 login_needed ? LOGIN_NEEDED : LOGIN_NOT_NEEDED, | |
| 203 LOGIN_NEEDED_BOUNDARY); | |
| 204 if (login_needed) { | |
| 205 ui_->PromptSyncSetup(this); | |
| 206 // We'll get called back in OnSyncSetupResult | |
| 207 } else { | |
| 208 EndLogin(true); | |
| 209 } | |
| 210 } | |
| 211 | |
| 212 void AppNotifyChannelSetup::EndLogin(bool success) { | |
| 213 CHECK_EQ(LOGIN_STARTED, state_); | |
| 214 UMA_HISTOGRAM_ENUMERATION("AppNotify.ChannelSetupLoginResult", | |
| 215 success ? LOGIN_SUCCESS : LOGIN_FAILURE, | |
| 216 LOGIN_SUCCESS_BOUNDARY); | |
| 217 if (success) { | |
| 218 state_ = LOGIN_DONE; | |
| 219 BeginGetAccessToken(); | |
| 220 } else { | |
| 221 state_ = ERROR_STATE; | |
| 222 ReportResult("", USER_CANCELLED); | |
| 223 } | |
| 224 } | |
| 225 | |
| 226 void AppNotifyChannelSetup::BeginGetAccessToken() { | |
| 227 CHECK_EQ(LOGIN_DONE, state_); | |
| 228 state_ = FETCH_ACCESS_TOKEN_STARTED; | |
| 229 | |
| 230 oauth2_fetcher_.reset(new OAuth2AccessTokenFetcher( | |
| 231 this, profile_->GetRequestContext())); | |
| 232 std::vector<std::string> scopes; | |
| 233 scopes.push_back(GaiaUrls::GetInstance()->oauth1_login_scope()); | |
| 234 scopes.push_back(kOAuth2IssueTokenScope); | |
| 235 TokenService* token_service = TokenServiceFactory::GetForProfile(profile_); | |
| 236 oauth2_fetcher_->Start( | |
| 237 GaiaUrls::GetInstance()->oauth2_chrome_client_id(), | |
| 238 GaiaUrls::GetInstance()->oauth2_chrome_client_secret(), | |
| 239 token_service->GetOAuth2LoginRefreshToken(), | |
| 240 scopes); | |
| 241 } | |
| 242 | |
| 243 void AppNotifyChannelSetup::EndGetAccessToken(bool success) { | |
| 244 CHECK_EQ(FETCH_ACCESS_TOKEN_STARTED, state_); | |
| 245 if (success) { | |
| 246 state_ = FETCH_ACCESS_TOKEN_DONE; | |
| 247 BeginRecordGrant(); | |
| 248 } else if (!oauth2_access_token_failure_) { | |
| 249 oauth2_access_token_failure_ = true; | |
| 250 // If access token generation fails, then it means somehow the | |
| 251 // OAuth2 login scoped token became invalid. One way this can happen | |
| 252 // is if a user explicitly revoked access to Google Chrome from | |
| 253 // Google Accounts page. In such a case, we should try to show the | |
| 254 // login setup again to the user, but only if we have not already | |
| 255 // done so once (to avoid infinite loop). | |
| 256 state_ = INITIAL; | |
| 257 BeginLogin(); | |
| 258 } else { | |
| 259 state_ = ERROR_STATE; | |
| 260 ReportResult("", INTERNAL_ERROR); | |
| 261 } | |
| 262 } | |
| 263 | |
| 264 void AppNotifyChannelSetup::BeginRecordGrant() { | |
| 265 CHECK_EQ(FETCH_ACCESS_TOKEN_DONE, state_); | |
| 266 state_ = RECORD_GRANT_STARTED; | |
| 267 | |
| 268 GURL url = GetOAuth2IssueTokenURL(); | |
| 269 std::string body = MakeOAuth2IssueTokenBody(client_id_, extension_id_); | |
| 270 | |
| 271 url_fetcher_.reset(CreateURLFetcher(url, body, oauth2_access_token_)); | |
| 272 url_fetcher_->Start(); | |
| 273 } | |
| 274 | |
| 275 void AppNotifyChannelSetup::EndRecordGrant(const net::URLFetcher* source) { | |
| 276 CHECK_EQ(RECORD_GRANT_STARTED, state_); | |
| 277 | |
| 278 net::URLRequestStatus status = source->GetStatus(); | |
| 279 | |
| 280 if (status.status() == net::URLRequestStatus::SUCCESS) { | |
| 281 if (source->GetResponseCode() == net::HTTP_OK) { | |
| 282 state_ = RECORD_GRANT_DONE; | |
| 283 BeginGetChannelId(); | |
| 284 } else { | |
| 285 // Successfully done with HTTP request, but got an explicit error. | |
| 286 state_ = ERROR_STATE; | |
| 287 ReportResult("", AUTH_ERROR); | |
| 288 } | |
| 289 } else { | |
| 290 // Could not do HTTP request. | |
| 291 state_ = ERROR_STATE; | |
| 292 ReportResult("", INTERNAL_ERROR); | |
| 293 } | |
| 294 } | |
| 295 | |
| 296 void AppNotifyChannelSetup::BeginGetChannelId() { | |
| 297 CHECK_EQ(RECORD_GRANT_DONE, state_); | |
| 298 state_ = CHANNEL_ID_SETUP_STARTED; | |
| 299 | |
| 300 GURL url = GetCWSChannelServiceURL(); | |
| 301 | |
| 302 url_fetcher_.reset(CreateURLFetcher(url, "", oauth2_access_token_)); | |
| 303 url_fetcher_->Start(); | |
| 304 } | |
| 305 | |
| 306 void AppNotifyChannelSetup::EndGetChannelId(const net::URLFetcher* source) { | |
| 307 CHECK_EQ(CHANNEL_ID_SETUP_STARTED, state_); | |
| 308 net::URLRequestStatus status = source->GetStatus(); | |
| 309 | |
| 310 if (status.status() == net::URLRequestStatus::SUCCESS) { | |
| 311 if (source->GetResponseCode() == net::HTTP_OK) { | |
| 312 std::string data; | |
| 313 source->GetResponseAsString(&data); | |
| 314 std::string channel_id; | |
| 315 bool result = ParseCWSChannelServiceResponse(data, &channel_id); | |
| 316 if (result) { | |
| 317 state_ = CHANNEL_ID_SETUP_DONE; | |
| 318 ReportResult(channel_id, NONE); | |
| 319 } else { | |
| 320 state_ = ERROR_STATE; | |
| 321 ReportResult("", INTERNAL_ERROR); | |
| 322 } | |
| 323 } else { | |
| 324 // Successfully done with HTTP request, but got an explicit error. | |
| 325 state_ = ERROR_STATE; | |
| 326 ReportResult("", AUTH_ERROR); | |
| 327 } | |
| 328 } else { | |
| 329 // Could not do HTTP request. | |
| 330 state_ = ERROR_STATE; | |
| 331 ReportResult("", INTERNAL_ERROR); | |
| 332 } | |
| 333 } | |
| 334 | |
| 335 void AppNotifyChannelSetup::ReportResult( | |
| 336 const std::string& channel_id, | |
| 337 SetupError error) { | |
| 338 CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | |
| 339 CHECK(state_ == CHANNEL_ID_SETUP_DONE || state_ == ERROR_STATE); | |
| 340 | |
| 341 UMA_HISTOGRAM_ENUMERATION("AppNotification.ChannelSetupFinalResult", | |
| 342 error, SETUP_ERROR_BOUNDARY); | |
| 343 if (delegate_.get()) { | |
| 344 delegate_->AppNotifyChannelSetupComplete( | |
| 345 channel_id, GetErrorString(error), this); | |
| 346 } | |
| 347 Release(); // Matches AddRef in Start. | |
| 348 } | |
| 349 | |
| 350 // static | |
| 351 std::string AppNotifyChannelSetup::GetErrorString(SetupError error) { | |
| 352 switch (error) { | |
| 353 case NONE: return ""; | |
| 354 case AUTH_ERROR: return kChannelSetupAuthError; | |
| 355 case INTERNAL_ERROR: return kChannelSetupInternalError; | |
| 356 case USER_CANCELLED: return kChannelSetupCanceledByUser; | |
| 357 case SETUP_ERROR_BOUNDARY: { | |
| 358 CHECK(false); | |
| 359 break; | |
| 360 } | |
| 361 } | |
| 362 CHECK(false) << "Unhandled enum value"; | |
| 363 return std::string(); | |
| 364 } | |
| 365 | |
| 366 | |
| 367 // static | |
| 368 GURL AppNotifyChannelSetup::GetCWSChannelServiceURL() { | |
| 369 CommandLine* command_line = CommandLine::ForCurrentProcess(); | |
| 370 if (command_line->HasSwitch(switches::kAppNotifyChannelServerURL)) { | |
| 371 std::string switch_value = command_line->GetSwitchValueASCII( | |
| 372 switches::kAppNotifyChannelServerURL); | |
| 373 GURL result(switch_value); | |
| 374 if (result.is_valid()) { | |
| 375 return result; | |
| 376 } else { | |
| 377 LOG(ERROR) << "Invalid value for " << | |
| 378 switches::kAppNotifyChannelServerURL; | |
| 379 } | |
| 380 } | |
| 381 return GURL(kCWSChannelServiceURL); | |
| 382 } | |
| 383 | |
| 384 // static | |
| 385 GURL AppNotifyChannelSetup::GetOAuth2IssueTokenURL() { | |
| 386 return GURL(kOAuth2IssueTokenURL); | |
| 387 } | |
| 388 | |
| 389 // static | |
| 390 std::string AppNotifyChannelSetup::MakeOAuth2IssueTokenBody( | |
| 391 const std::string& oauth_client_id, | |
| 392 const std::string& extension_id) { | |
| 393 return StringPrintf(kOAuth2IssueTokenBodyFormat, | |
| 394 kOAuth2IssueTokenScope, | |
| 395 net::EscapeUrlEncodedData(oauth_client_id, true).c_str(), | |
| 396 net::EscapeUrlEncodedData(extension_id, true).c_str()); | |
| 397 } | |
| 398 | |
| 399 // static | |
| 400 std::string AppNotifyChannelSetup::MakeAuthorizationHeader( | |
| 401 const std::string& auth_token) { | |
| 402 return StringPrintf(kAuthorizationHeaderFormat, auth_token.c_str()); | |
| 403 } | |
| 404 | |
| 405 // static | |
| 406 bool AppNotifyChannelSetup::ParseCWSChannelServiceResponse( | |
| 407 const std::string& data, std::string* result) { | |
| 408 scoped_ptr<base::Value> value(base::JSONReader::Read(data)); | |
| 409 if (!value.get() || value->GetType() != base::Value::TYPE_DICTIONARY) | |
| 410 return false; | |
| 411 | |
| 412 DictionaryValue* dict = static_cast<DictionaryValue*>(value.get()); | |
| 413 return dict->GetString("id", result); | |
| 414 } | |
| 415 | |
| 416 } // namespace extensions | |
| OLD | NEW |