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 <string>
6
7#include "jingle/notifier/communicator/single_login_attempt.h"
8
9#include "base/basictypes.h"
10#include "base/logging.h"
11#include "base/strings/string_number_conversions.h"
12#include "base/strings/string_split.h"
13#include "jingle/notifier/base/const_communicator.h"
14#include "jingle/notifier/base/gaia_token_pre_xmpp_auth.h"
15#include "jingle/notifier/listener/xml_element_util.h"
16#include "net/base/host_port_pair.h"
17#include "talk/xmllite/xmlelement.h"
18#include "talk/xmpp/constants.h"
19#include "talk/xmpp/xmppclientsettings.h"
20
21namespace notifier {
22
23SingleLoginAttempt::Delegate::~Delegate() {}
24
25SingleLoginAttempt::SingleLoginAttempt(const LoginSettings& login_settings,
26                                       Delegate* delegate)
27    : login_settings_(login_settings),
28      delegate_(delegate),
29      settings_list_(
30          MakeConnectionSettingsList(login_settings_.GetServers(),
31                                     login_settings_.try_ssltcp_first())),
32      current_settings_(settings_list_.begin()) {
33  if (settings_list_.empty()) {
34    NOTREACHED();
35    return;
36  }
37  TryConnect(*current_settings_);
38}
39
40SingleLoginAttempt::~SingleLoginAttempt() {}
41
42// In the code below, we assume that calling a delegate method may end
43// up in ourselves being deleted, so we always call it last.
44//
45// TODO(akalin): Add unit tests to enforce the behavior above.
46
47void SingleLoginAttempt::OnConnect(
48    base::WeakPtr<buzz::XmppTaskParentInterface> base_task) {
49  DVLOG(1) << "Connected to " << current_settings_->ToString();
50  delegate_->OnConnect(base_task);
51}
52
53namespace {
54
55// This function is more permissive than
56// net::HostPortPair::FromString().  If the port is missing or
57// unparseable, it assumes the default XMPP port.  The hostname may be
58// empty.
59net::HostPortPair ParseRedirectText(const std::string& redirect_text) {
60  std::vector<std::string> parts;
61  base::SplitString(redirect_text, ':', &parts);
62  net::HostPortPair redirect_server;
63  redirect_server.set_port(kDefaultXmppPort);
64  if (parts.empty()) {
65    return redirect_server;
66  }
67  redirect_server.set_host(parts[0]);
68  if (parts.size() <= 1) {
69    return redirect_server;
70  }
71  // Try to parse the port, falling back to kDefaultXmppPort.
72  int port = kDefaultXmppPort;
73  if (!base::StringToInt(parts[1], &port)) {
74    port = kDefaultXmppPort;
75  }
76  if (port <= 0 || port > kuint16max) {
77    port = kDefaultXmppPort;
78  }
79  redirect_server.set_port(port);
80  return redirect_server;
81}
82
83}  // namespace
84
85void SingleLoginAttempt::OnError(buzz::XmppEngine::Error error, int subcode,
86                                 const buzz::XmlElement* stream_error) {
87  DVLOG(1) << "Error: " << error << ", subcode: " << subcode
88           << (stream_error
89                   ? (", stream error: " + XmlElementToString(*stream_error))
90                   : std::string());
91
92  DCHECK_EQ(error == buzz::XmppEngine::ERROR_STREAM, stream_error != NULL);
93
94  // Check for redirection.  We expect something like:
95  //
96  // <stream:error><see-other-host xmlns="urn:ietf:params:xml:ns:xmpp-streams"/><str:text xmlns:str="urn:ietf:params:xml:ns:xmpp-streams">talk.google.com</str:text></stream:error> [2]
97  //
98  // There are some differences from the spec [1]:
99  //
100  //   - we expect a separate text element with the redirection info
101  //     (which is the format Google Talk's servers use), whereas the
102  //     spec puts the redirection info directly in the see-other-host
103  //     element;
104  //   - we check for redirection only during login, whereas the
105  //     server can send down a redirection at any time according to
106  //     the spec. (TODO(akalin): Figure out whether we need to handle
107  //     redirection at any other point.)
108  //
109  // [1]: http://xmpp.org/internet-drafts/draft-saintandre-rfc3920bis-08.html#streams-error-conditions-see-other-host
110  // [2]: http://forums.miranda-im.org/showthread.php?24376-GoogleTalk-drops
111  if (stream_error) {
112    const buzz::XmlElement* other =
113        stream_error->FirstNamed(buzz::QN_XSTREAM_SEE_OTHER_HOST);
114    if (other) {
115      const buzz::XmlElement* text =
116          stream_error->FirstNamed(buzz::QN_XSTREAM_TEXT);
117      if (text) {
118        // Yep, its a "stream:error" with "see-other-host" text,
119        // let's parse out the server:port, and then reconnect
120        // with that.
121        const net::HostPortPair& redirect_server =
122            ParseRedirectText(text->BodyText());
123        // ParseRedirectText shouldn't return a zero port.
124        DCHECK_NE(redirect_server.port(), 0u);
125        // If we don't have a host, ignore the redirection and treat
126        // it like a regular error.
127        if (!redirect_server.host().empty()) {
128          delegate_->OnRedirect(
129              ServerInformation(
130                  redirect_server,
131                  current_settings_->ssltcp_support));
132          // May be deleted at this point.
133          return;
134        }
135      }
136    }
137  }
138
139  if (error == buzz::XmppEngine::ERROR_UNAUTHORIZED) {
140    DVLOG(1) << "Credentials rejected";
141    delegate_->OnCredentialsRejected();
142    return;
143  }
144
145  if (current_settings_ == settings_list_.end()) {
146    NOTREACHED();
147    return;
148  }
149
150  ++current_settings_;
151  if (current_settings_ == settings_list_.end()) {
152    DVLOG(1) << "Could not connect to any XMPP server";
153    delegate_->OnSettingsExhausted();
154    return;
155  }
156
157  TryConnect(*current_settings_);
158}
159
160void SingleLoginAttempt::TryConnect(
161    const ConnectionSettings& connection_settings) {
162  DVLOG(1) << "Trying to connect to " << connection_settings.ToString();
163  // Copy the user settings and fill in the connection parameters from
164  // |connection_settings|.
165  buzz::XmppClientSettings client_settings = login_settings_.user_settings();
166  connection_settings.FillXmppClientSettings(&client_settings);
167
168  buzz::Jid jid(client_settings.user(), client_settings.host(),
169                buzz::STR_EMPTY);
170  buzz::PreXmppAuth* pre_xmpp_auth =
171      new GaiaTokenPreXmppAuth(
172          jid.Str(), client_settings.auth_token(),
173          client_settings.token_service(),
174          login_settings_.auth_mechanism());
175  xmpp_connection_.reset(
176      new XmppConnection(client_settings,
177                         login_settings_.request_context_getter(),
178                         this,
179                         pre_xmpp_auth));
180}
181
182}  // namespace notifier
183