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 |