OLD | NEW |
| (Empty) |
1 // Copyright (c) 2009 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 <algorithm> | |
6 #include <string> | |
7 #include <vector> | |
8 | |
9 #include "chrome/browser/sync/notifier/communicator/single_login_attempt.h" | |
10 | |
11 #include "base/logging.h" | |
12 #include "chrome/browser/sync/notifier/communicator/connection_options.h" | |
13 #include "chrome/browser/sync/notifier/communicator/connection_settings.h" | |
14 #include "chrome/browser/sync/notifier/communicator/const_communicator.h" | |
15 #include "chrome/browser/sync/notifier/communicator/login_failure.h" | |
16 #include "chrome/browser/sync/notifier/communicator/login_settings.h" | |
17 #include "chrome/browser/sync/notifier/communicator/product_info.h" | |
18 #include "chrome/browser/sync/notifier/communicator/xmpp_connection_generator.h" | |
19 #include "chrome/browser/sync/notifier/communicator/xmpp_socket_adapter.h" | |
20 #include "talk/base/asynchttprequest.h" | |
21 #include "talk/base/firewallsocketserver.h" | |
22 #include "talk/base/signalthread.h" | |
23 #include "talk/base/taskrunner.h" | |
24 #include "talk/base/winsock_initializer.h" | |
25 #include "talk/xmllite/xmlelement.h" | |
26 #include "talk/xmpp/saslcookiemechanism.h" | |
27 #include "talk/xmpp/saslhandler.h" | |
28 #include "talk/xmpp/xmppclient.h" | |
29 #include "talk/xmpp/xmppclientsettings.h" | |
30 #include "talk/xmpp/xmppconstants.h" | |
31 | |
32 namespace notifier { | |
33 | |
34 static void GetClientErrorInformation( | |
35 buzz::XmppClient* client, | |
36 buzz::XmppEngine::Error* error, | |
37 int* subcode, | |
38 buzz::XmlElement** stream_error) { | |
39 ASSERT(client != NULL); | |
40 ASSERT(error && subcode && stream_error); | |
41 | |
42 *error = client->GetError(subcode); | |
43 | |
44 *stream_error = NULL; | |
45 if (*error == buzz::XmppEngine::ERROR_STREAM) { | |
46 const buzz::XmlElement* error_element = client->GetStreamError(); | |
47 if (error_element) { | |
48 *stream_error = new buzz::XmlElement(*error_element); | |
49 } | |
50 } | |
51 } | |
52 | |
53 namespace { | |
54 | |
55 const char kGaiaAuthMechanism[] = "X-GOOGLE-TOKEN"; | |
56 | |
57 // This class looks for the X-GOOGLE-TOKEN auth mechanism and uses | |
58 // that instead of the default auth mechanism (PLAIN). | |
59 class GaiaOnlySaslHandler : public buzz::SaslHandler { | |
60 public: | |
61 GaiaOnlySaslHandler( | |
62 const std::string& username, | |
63 const std::string& token, | |
64 const std::string& token_service) | |
65 : username_(username), | |
66 token_(token), | |
67 token_service_(token_service) {} | |
68 | |
69 virtual std::string ChooseBestSaslMechanism( | |
70 const std::vector<std::string> & mechanisms, bool encrypted) { | |
71 return (std::find(mechanisms.begin(), | |
72 mechanisms.end(), kGaiaAuthMechanism) != | |
73 mechanisms.end()) ? kGaiaAuthMechanism : ""; | |
74 } | |
75 | |
76 virtual buzz::SaslMechanism* CreateSaslMechanism( | |
77 const std::string& mechanism) { | |
78 return | |
79 (mechanism == kGaiaAuthMechanism) ? | |
80 new buzz::SaslCookieMechanism( | |
81 kGaiaAuthMechanism, username_, token_, token_service_) | |
82 : NULL; | |
83 } | |
84 | |
85 virtual bool GetTlsServerInfo(const talk_base::SocketAddress& server, | |
86 std::string* tls_server_hostname, | |
87 std::string* tls_server_domain) { | |
88 std::string server_ip = server.IPAsString(); | |
89 if ((server_ip == buzz::STR_TALK_GOOGLE_COM) || | |
90 (server_ip == buzz::STR_TALKX_L_GOOGLE_COM)) { | |
91 // For Gaia auth, the talk.google.com server expects you to use | |
92 // "gmail.com" in the stream, and expects the domain certificate | |
93 // to be "gmail.com" as well. | |
94 *tls_server_hostname = buzz::STR_GMAIL_COM; | |
95 *tls_server_domain = buzz::STR_GMAIL_COM; | |
96 return true; | |
97 } | |
98 return false; | |
99 } | |
100 | |
101 private: | |
102 std::string username_, token_, token_service_; | |
103 }; | |
104 | |
105 } // namespace | |
106 | |
107 SingleLoginAttempt::SingleLoginAttempt(talk_base::Task* parent, | |
108 LoginSettings* login_settings, | |
109 bool successful_connection) | |
110 : talk_base::Task(parent), | |
111 state_(buzz::XmppEngine::STATE_NONE), | |
112 code_(buzz::XmppEngine::ERROR_NONE), | |
113 subcode_(0), | |
114 need_authentication_(false), | |
115 certificate_expired_(false), | |
116 cookie_refreshed_(false), | |
117 successful_connection_(successful_connection), | |
118 login_settings_(login_settings), | |
119 client_(NULL) { | |
120 #if defined(OS_WIN) | |
121 talk_base::EnsureWinsockInit(); | |
122 #endif | |
123 connection_generator_.reset(new XmppConnectionGenerator( | |
124 this, | |
125 &login_settings_->connection_options(), | |
126 login_settings_->proxy_only(), | |
127 login_settings_->server_list(), | |
128 login_settings_->server_count())); | |
129 | |
130 connection_generator_->SignalExhaustedSettings.connect( | |
131 this, | |
132 &SingleLoginAttempt::OnAttemptedAllConnections); | |
133 connection_generator_->SignalNewSettings.connect( | |
134 this, | |
135 &SingleLoginAttempt::DoLogin); | |
136 } | |
137 | |
138 SingleLoginAttempt::~SingleLoginAttempt() { | |
139 // If this assertion goes off, it means that "Stop()" didn't get called like | |
140 // it should have been. | |
141 ASSERT(client_ == NULL); | |
142 } | |
143 | |
144 bool SingleLoginAttempt::auto_reconnect() const { | |
145 return login_settings_->connection_options().auto_reconnect(); | |
146 } | |
147 | |
148 const talk_base::ProxyInfo& SingleLoginAttempt::proxy() const { | |
149 ASSERT(connection_generator_.get()); | |
150 return connection_generator_->proxy(); | |
151 } | |
152 | |
153 int SingleLoginAttempt::ProcessStart() { | |
154 ASSERT(GetState() == talk_base::Task::STATE_START); | |
155 connection_generator_->StartGenerating(); | |
156 | |
157 // After being started, this class is callback driven and does signaling from | |
158 // those callbacks (with checks to see if it is done if it may be called back | |
159 // from something that isn't a child task). | |
160 return talk_base::Task::STATE_BLOCKED; | |
161 } | |
162 | |
163 void SingleLoginAttempt::Stop() { | |
164 ClearClient(); | |
165 talk_base::Task::Stop(); | |
166 | |
167 // No more signals should happen after being stopped. This is needed because | |
168 // some of these signals happen due to other components doing signaling which | |
169 // may continue running even though this task is stopped. | |
170 SignalUnexpectedDisconnect.disconnect_all(); | |
171 SignalRedirect.disconnect_all(); | |
172 SignalLoginFailure.disconnect_all(); | |
173 SignalNeedAutoReconnect.disconnect_all(); | |
174 SignalClientStateChange.disconnect_all(); | |
175 } | |
176 | |
177 void SingleLoginAttempt::OnAttemptedAllConnections( | |
178 bool successfully_resolved_dns, | |
179 int first_dns_error) { | |
180 | |
181 // Maybe we needed proxy authentication? | |
182 if (need_authentication_) { | |
183 LoginFailure failure(LoginFailure::PROXY_AUTHENTICATION_ERROR); | |
184 SignalLoginFailure(failure); | |
185 return; | |
186 } | |
187 | |
188 if (certificate_expired_) { | |
189 LoginFailure failure(LoginFailure::CERTIFICATE_EXPIRED_ERROR); | |
190 SignalLoginFailure(failure); | |
191 return; | |
192 } | |
193 | |
194 if (!successfully_resolved_dns) { | |
195 code_ = buzz::XmppEngine::ERROR_SOCKET; | |
196 subcode_ = first_dns_error; | |
197 } | |
198 | |
199 LOG(INFO) << "Connection failed with error " << code_; | |
200 | |
201 // We were connected and we had a problem. | |
202 if (successful_connection_ && auto_reconnect()) { | |
203 SignalNeedAutoReconnect(); | |
204 // Expect to be deleted at this point. | |
205 return; | |
206 } | |
207 | |
208 DiagnoseConnectionError(); | |
209 } | |
210 | |
211 void SingleLoginAttempt::UseNextConnection() { | |
212 ASSERT(connection_generator_.get() != NULL); | |
213 ClearClient(); | |
214 connection_generator_->UseNextConnection(); | |
215 } | |
216 | |
217 void SingleLoginAttempt::UseCurrentConnection() { | |
218 ASSERT(connection_generator_.get() != NULL); | |
219 ClearClient(); | |
220 connection_generator_->UseCurrentConnection(); | |
221 } | |
222 | |
223 void SingleLoginAttempt::DoLogin( | |
224 const ConnectionSettings& connection_settings) { | |
225 if (client_) { | |
226 return; | |
227 } | |
228 | |
229 buzz::XmppClientSettings client_settings; | |
230 // Set the user settings portion. | |
231 *static_cast<buzz::XmppClientSettings*>(&client_settings) = | |
232 login_settings_->user_settings(); | |
233 // Fill in the rest of the client settings. | |
234 connection_settings.FillXmppClientSettings(&client_settings); | |
235 | |
236 client_ = new buzz::XmppClient(this); | |
237 SignalLogInput.repeat(client_->SignalLogInput); | |
238 SignalLogOutput.repeat(client_->SignalLogOutput); | |
239 | |
240 // Listen for connection progress. | |
241 client_->SignalStateChange.connect(this, | |
242 &SingleLoginAttempt::OnClientStateChange); | |
243 | |
244 // Transition to "start". | |
245 OnClientStateChange(buzz::XmppEngine::STATE_START); | |
246 // Start connecting. | |
247 client_->Connect(client_settings, login_settings_->lang(), | |
248 CreateSocket(client_settings), | |
249 NULL, | |
250 CreateSaslHandler(client_settings)); | |
251 client_->Start(); | |
252 } | |
253 | |
254 void SingleLoginAttempt::OnAuthenticationError() { | |
255 // We can check this flag later if all connection options fail. | |
256 need_authentication_ = true; | |
257 } | |
258 | |
259 void SingleLoginAttempt::OnCertificateExpired() { | |
260 // We can check this flag later if all connection options fail. | |
261 certificate_expired_ = true; | |
262 } | |
263 | |
264 buzz::AsyncSocket* SingleLoginAttempt::CreateSocket( | |
265 const buzz::XmppClientSettings& xcs) { | |
266 bool allow_unverified_certs = | |
267 login_settings_->connection_options().allow_unverified_certs(); | |
268 XmppSocketAdapter* adapter = new XmppSocketAdapter(xcs, | |
269 allow_unverified_certs); | |
270 adapter->SignalAuthenticationError.connect( | |
271 this, | |
272 &SingleLoginAttempt::OnAuthenticationError); | |
273 if (login_settings_->firewall()) { | |
274 adapter->set_firewall(true); | |
275 } | |
276 return adapter; | |
277 } | |
278 | |
279 buzz::SaslHandler* SingleLoginAttempt::CreateSaslHandler( | |
280 const buzz::XmppClientSettings& xcs) { | |
281 buzz::Jid jid(xcs.user(), xcs.host(), buzz::STR_EMPTY); | |
282 return new GaiaOnlySaslHandler( | |
283 jid.Str(), xcs.auth_cookie(), xcs.token_service()); | |
284 } | |
285 | |
286 void SingleLoginAttempt::OnFreshAuthCookie(const std::string& auth_cookie) { | |
287 // Remember this is a fresh cookie. | |
288 cookie_refreshed_ = true; | |
289 | |
290 // TODO(sync): do the cookie logic (part of which is in the #if 0 below). | |
291 | |
292 // The following code is what PhoneWindow does for the equivalent method. | |
293 #if 0 | |
294 // Save cookie | |
295 AccountInfo current(account_history_.current()); | |
296 current.set_auth_cookie(auth_cookie); | |
297 account_history_.set_current(current); | |
298 | |
299 // Calc next time to refresh cookie, between 5 and 10 days. The cookie has | |
300 // 14 days of life; this gives at least 4 days of retries before the current | |
301 // cookie expires, maximizing the chance of having a valid cookie next time | |
302 // the connection servers go down. | |
303 FTULL now; | |
304 | |
305 // NOTE: The following line is win32. Address this when implementing this | |
306 // code (doing "the cookie logic"). | |
307 GetSystemTimeAsFileTime(&(now.ft)); | |
308 ULONGLONG five_days = (ULONGLONG)10000 * 1000 * 60 * 60 * 24 * 5; // 5 days | |
309 ULONGLONG random = (ULONGLONG)10000 * // get to 100 ns units | |
310 ((rand() % (5 * 24 * 60)) * (60 * 1000) + // random min. in 5 day period | |
311 (rand() % 1000) * 60); // random 1/1000th of a minute | |
312 next_cookie_refresh_ = now.ull + five_days + random; // 5-10 days | |
313 #endif | |
314 } | |
315 | |
316 void SingleLoginAttempt::DiagnoseConnectionError() { | |
317 switch (code_) { | |
318 case buzz::XmppEngine::ERROR_MISSING_USERNAME: | |
319 case buzz::XmppEngine::ERROR_NETWORK_TIMEOUT: | |
320 case buzz::XmppEngine::ERROR_DOCUMENT_CLOSED: | |
321 case buzz::XmppEngine::ERROR_BIND: | |
322 case buzz::XmppEngine::ERROR_AUTH: | |
323 case buzz::XmppEngine::ERROR_TLS: | |
324 case buzz::XmppEngine::ERROR_UNAUTHORIZED: | |
325 case buzz::XmppEngine::ERROR_VERSION: | |
326 case buzz::XmppEngine::ERROR_STREAM: | |
327 case buzz::XmppEngine::ERROR_XML: | |
328 case buzz::XmppEngine::ERROR_NONE: | |
329 default: { | |
330 LoginFailure failure(LoginFailure::XMPP_ERROR, code_, subcode_); | |
331 SignalLoginFailure(failure); | |
332 return; | |
333 } | |
334 | |
335 // The following errors require diagnosistics: | |
336 // * spurious close of connection | |
337 // * socket errors after auth | |
338 case buzz::XmppEngine::ERROR_CONNECTION_CLOSED: | |
339 case buzz::XmppEngine::ERROR_SOCKET: | |
340 break; | |
341 } | |
342 | |
343 talk_base::AsyncHttpRequest *http_request = | |
344 new talk_base::AsyncHttpRequest(GetUserAgentString()); | |
345 http_request->set_host("www.google.com"); | |
346 http_request->set_port(80); | |
347 http_request->set_secure(false); | |
348 http_request->request().path = "/"; | |
349 http_request->request().verb = talk_base::HV_GET; | |
350 | |
351 talk_base::ProxyInfo proxy; | |
352 ASSERT(connection_generator_.get() != NULL); | |
353 if (connection_generator_.get()) { | |
354 proxy = connection_generator_->proxy(); | |
355 } | |
356 http_request->set_proxy(proxy); | |
357 http_request->set_firewall(login_settings_->firewall()); | |
358 | |
359 http_request->SignalWorkDone.connect(this, | |
360 &SingleLoginAttempt::OnHttpTestDone); | |
361 http_request->Start(); | |
362 http_request->Release(); | |
363 } | |
364 | |
365 void SingleLoginAttempt::OnHttpTestDone(talk_base::SignalThread* thread) { | |
366 ASSERT(thread != NULL); | |
367 | |
368 talk_base::AsyncHttpRequest* request = | |
369 static_cast<talk_base::AsyncHttpRequest*>(thread); | |
370 | |
371 if (request->response().scode == 200) { | |
372 // We were able to do an HTTP GET of www.google.com:80 | |
373 | |
374 // | |
375 // The original error should be reported | |
376 // | |
377 LoginFailure failure(LoginFailure::XMPP_ERROR, code_, subcode_); | |
378 SignalLoginFailure(failure); | |
379 return; | |
380 } | |
381 | |
382 // Otherwise lets transmute the error into ERROR_SOCKET, and put the subcode | |
383 // as an indicator of what we think the problem might be. | |
384 | |
385 #if 0 | |
386 // TODO(sync): determine if notifier has an analogous situation. | |
387 | |
388 // | |
389 // We weren't able to do an HTTP GET of www.google.com:80 | |
390 // | |
391 GAutoupdater::Version version_logged_in(g_options.version_logged_in()); | |
392 GAutoupdater::Version version_installed(GetProductVersion().c_str()); | |
393 if (version_logged_in < version_installed) { | |
394 // | |
395 // Google Talk has been updated and can no longer connect to the Google | |
396 // Talk Service. Your firewall is probably not allowing the new version of | |
397 // Google Talk to connect to the internet. Please adjust your firewall | |
398 // settings to allow the new version of Google Talk to connect to the | |
399 // internet. | |
400 // | |
401 // We'll use the "error=1" to help figure this out for now. | |
402 // | |
403 LoginFailure failure(LoginFailure::XMPP_ERROR, | |
404 buzz::XmppEngine::ERROR_SOCKET, | |
405 1); | |
406 SignalLoginFailure(failure); | |
407 return; | |
408 } | |
409 #endif | |
410 | |
411 // | |
412 // Any other checking we can add here? | |
413 // | |
414 | |
415 // | |
416 // Google Talk is unable to use your internet connection. Either your network | |
417 // isn't configured or Google Talk is being blocked by a local firewall. | |
418 // | |
419 // We'll use the "error=0" to help figure this out for now | |
420 // | |
421 LoginFailure failure(LoginFailure::XMPP_ERROR, | |
422 buzz::XmppEngine::ERROR_SOCKET, | |
423 0); | |
424 SignalLoginFailure(failure); | |
425 } | |
426 | |
427 void SingleLoginAttempt::OnClientStateChange(buzz::XmppEngine::State state) { | |
428 if (state_ == state) | |
429 return; | |
430 | |
431 buzz::XmppEngine::State previous_state = state_; | |
432 state_ = state; | |
433 | |
434 switch (state) { | |
435 case buzz::XmppEngine::STATE_NONE: | |
436 case buzz::XmppEngine::STATE_START: | |
437 case buzz::XmppEngine::STATE_OPENING: | |
438 // Do nothing. | |
439 break; | |
440 case buzz::XmppEngine::STATE_OPEN: | |
441 successful_connection_ = true; | |
442 break; | |
443 case buzz::XmppEngine::STATE_CLOSED: | |
444 OnClientStateChangeClosed(previous_state); | |
445 break; | |
446 } | |
447 SignalClientStateChange(state); | |
448 if (state_ == buzz::XmppEngine::STATE_CLOSED) { | |
449 OnClientStateChange(buzz::XmppEngine::STATE_NONE); | |
450 } | |
451 } | |
452 | |
453 void SingleLoginAttempt::ClearClient() { | |
454 if (client_ != NULL) { | |
455 client_->Disconnect(); | |
456 | |
457 // If this assertion goes off, it means that the disconnect didn't occur | |
458 // properly. See SingleLoginAttempt::OnClientStateChange, | |
459 // case XmppEngine::STATE_CLOSED | |
460 ASSERT(client_ == NULL); | |
461 } | |
462 } | |
463 | |
464 void SingleLoginAttempt::OnClientStateChangeClosed( | |
465 buzz::XmppEngine::State previous_state) { | |
466 buzz::XmppEngine::Error error = buzz::XmppEngine::ERROR_NONE; | |
467 int error_subcode = 0; | |
468 buzz::XmlElement* stream_error_ptr; | |
469 GetClientErrorInformation(client_, | |
470 &error, | |
471 &error_subcode, | |
472 &stream_error_ptr); | |
473 scoped_ptr<buzz::XmlElement> stream_error(stream_error_ptr); | |
474 | |
475 client_->SignalStateChange.disconnect(this); | |
476 client_ = NULL; | |
477 | |
478 if (error == buzz::XmppEngine::ERROR_NONE) { | |
479 SignalLogoff(); | |
480 return; | |
481 } else if (previous_state == buzz::XmppEngine::STATE_OPEN) { | |
482 // Handler should attempt reconnect. | |
483 SignalUnexpectedDisconnect(); | |
484 return; | |
485 } else { | |
486 HandleConnectionError(error, error_subcode, stream_error.get()); | |
487 } | |
488 } | |
489 | |
490 void SingleLoginAttempt::HandleConnectionPasswordError() { | |
491 LOG(INFO) << "SingleLoginAttempt::HandleConnectionPasswordError"; | |
492 LoginFailure failure(LoginFailure::XMPP_ERROR, code_, subcode_); | |
493 SignalLoginFailure(failure); | |
494 } | |
495 | |
496 void SingleLoginAttempt::HandleConnectionError( | |
497 buzz::XmppEngine::Error code, | |
498 int subcode, | |
499 const buzz::XmlElement* stream_error) { | |
500 LOG(INFO) << "(" << code << ", " << subcode << ")"; | |
501 | |
502 // Save off the error code information, so we can use it to tell the user | |
503 // what went wrong if all else fails. | |
504 code_ = code; | |
505 subcode_ = subcode; | |
506 if ((code_ == buzz::XmppEngine::ERROR_UNAUTHORIZED) || | |
507 (code_ == buzz::XmppEngine::ERROR_MISSING_USERNAME)) { | |
508 // There was a problem with credentials (username/password). | |
509 HandleConnectionPasswordError(); | |
510 return; | |
511 } | |
512 | |
513 // Unexpected disconnect, | |
514 // Unreachable host, | |
515 // Or internal server binding error - | |
516 // All these are temporary problems, so continue reconnecting. | |
517 | |
518 // GaiaAuth signals this directly via SignalCertificateExpired, but | |
519 // SChannelAdapter propagates the error through SocketWindow as a socket | |
520 // error. | |
521 if (code_ == buzz::XmppEngine::ERROR_SOCKET && | |
522 subcode_ == SEC_E_CERT_EXPIRED) { | |
523 certificate_expired_ = true; | |
524 } | |
525 | |
526 login_settings_->modifiable_user_settings()->set_resource(""); | |
527 | |
528 // Look for stream::error server redirection stanza "see-other-host". | |
529 if (stream_error) { | |
530 const buzz::XmlElement* other = | |
531 stream_error->FirstNamed(buzz::QN_XSTREAM_SEE_OTHER_HOST); | |
532 if (other) { | |
533 const buzz::XmlElement* text = | |
534 stream_error->FirstNamed(buzz::QN_XSTREAM_TEXT); | |
535 if (text) { | |
536 // Yep, its a "stream:error" with "see-other-host" text, let's parse | |
537 // out the server:port, and then reconnect with that. | |
538 const std::string& redirect = text->BodyText(); | |
539 size_t colon = redirect.find(":"); | |
540 int redirect_port = kDefaultXmppPort; | |
541 std::string redirect_server; | |
542 if (colon == std::string::npos) { | |
543 redirect_server = redirect; | |
544 } else { | |
545 redirect_server = redirect.substr(0, colon); | |
546 const std::string& port_text = redirect.substr(colon + 1); | |
547 std::istringstream ist(port_text); | |
548 ist >> redirect_port; | |
549 } | |
550 // We never allow a redirect to port 0. | |
551 if (redirect_port == 0) { | |
552 redirect_port = kDefaultXmppPort; | |
553 } | |
554 SignalRedirect(redirect_server, redirect_port); | |
555 // May be deleted at this point. | |
556 return; | |
557 } | |
558 } | |
559 } | |
560 | |
561 ASSERT(connection_generator_.get() != NULL); | |
562 if (!connection_generator_.get()) { | |
563 return; | |
564 } | |
565 | |
566 // Iterate to the next possible connection (still trying to connect). | |
567 UseNextConnection(); | |
568 } | |
569 | |
570 } // namespace notifier | |
OLD | NEW |