diff options
Diffstat (limited to 'jingle/notifier')
56 files changed, 5906 insertions, 0 deletions
diff --git a/jingle/notifier/DEPS b/jingle/notifier/DEPS new file mode 100644 index 0000000..2d7d13f --- /dev/null +++ b/jingle/notifier/DEPS @@ -0,0 +1,6 @@ +include_rules = [ + # notifier depends on libjingle. + "+talk/base", + "+talk/xmpp", + "+talk/xmllite", +] diff --git a/jingle/notifier/base/signal_thread_task.h b/jingle/notifier/base/signal_thread_task.h new file mode 100644 index 0000000..8a6ff27 --- /dev/null +++ b/jingle/notifier/base/signal_thread_task.h @@ -0,0 +1,96 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef JINGLE_NOTIFIER_BASE_SIGNAL_THREAD_TASK_H_ +#define JINGLE_NOTIFIER_BASE_SIGNAL_THREAD_TASK_H_ + +#include "base/logging.h" +#include "talk/base/common.h" +#include "talk/base/signalthread.h" +#include "talk/base/sigslot.h" +#include "talk/base/task.h" + +namespace notifier { + +template<class T> +class SignalThreadTask : public talk_base::Task, + public sigslot::has_slots<> { + public: + // Takes ownership of signal_thread. + SignalThreadTask(talk_base::Task* task_parent, T** signal_thread) + : talk_base::Task(task_parent), + signal_thread_(NULL), + finished_(false) { + SetSignalThread(signal_thread); + } + + virtual ~SignalThreadTask() { + ClearSignalThread(); + } + + virtual void Stop() { + Task::Stop(); + ClearSignalThread(); + } + + virtual int ProcessStart() { + DCHECK_EQ(GetState(), talk_base::Task::STATE_START); + signal_thread_->SignalWorkDone.connect( + this, + &SignalThreadTask<T>::OnWorkDone); + signal_thread_->Start(); + return talk_base::Task::STATE_RESPONSE; + } + + int ProcessResponse() { + if (!finished_) { + return talk_base::Task::STATE_BLOCKED; + } + SignalWorkDone(signal_thread_); + ClearSignalThread(); + return talk_base::Task::STATE_DONE; + } + + sigslot::signal1<T*> SignalWorkDone; + + private: + // Takes ownership of signal_thread. + void SetSignalThread(T** signal_thread) { + DCHECK(!signal_thread_); + DCHECK(signal_thread); + DCHECK(*signal_thread); + // No one should be listening to the signal thread for work done. + // They should be using this class instead. Unfortunately, we + // can't verify this. + + signal_thread_ = *signal_thread; + + // Helps callers not to use signal thread after this point since this class + // has taken ownership (and avoid the error of doing + // signal_thread->Start()). + *signal_thread = NULL; + } + + void OnWorkDone(talk_base::SignalThread* signal_thread) { + DCHECK_EQ(signal_thread, signal_thread_); + finished_ = true; + Wake(); + } + + void ClearSignalThread() { + if (signal_thread_) { + // Don't wait on the thread destruction, or we may deadlock. + signal_thread_->Destroy(false); + signal_thread_ = NULL; + } + } + + T* signal_thread_; + bool finished_; + DISALLOW_COPY_AND_ASSIGN(SignalThreadTask); +}; + +} // namespace notifier + +#endif // JINGLE_NOTIFIER_BASE_SIGNAL_THREAD_TASK_H_ diff --git a/jingle/notifier/base/sigslotrepeater.h b/jingle/notifier/base/sigslotrepeater.h new file mode 100644 index 0000000..cafb491 --- /dev/null +++ b/jingle/notifier/base/sigslotrepeater.h @@ -0,0 +1,83 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef JINGLE_NOTIFIER_BASE_SIGSLOTREPEATER_H_ +#define JINGLE_NOTIFIER_BASE_SIGSLOTREPEATER_H_ + +// Repeaters are both signals and slots, which are designed as intermediate +// pass-throughs for signals and slots which don't know about each other (for +// modularity or encapsulation). This eliminates the need to declare a signal +// handler whose sole purpose is to fire another signal. The repeater connects +// to the originating signal using the 'repeat' method. When the repeated +// signal fires, the repeater will also fire. + +#include "talk/base/sigslot.h" + +namespace sigslot { + +template<class mt_policy = SIGSLOT_DEFAULT_MT_POLICY> +class repeater0 : public signal0<mt_policy>, + public has_slots<mt_policy> { + public: + typedef signal0<mt_policy> base_type; + typedef repeater0<mt_policy> this_type; + + repeater0() { } + explicit repeater0(const this_type& s) : base_type(s) { } + + void reemit() { signal0<mt_policy>::emit(); } + void repeat(base_type &s) { s.connect(this, &this_type::reemit); } +}; + +template<class arg1_type, class mt_policy = SIGSLOT_DEFAULT_MT_POLICY> +class repeater1 : public signal1<arg1_type, mt_policy>, + public has_slots<mt_policy> { + public: + typedef signal1<arg1_type, mt_policy> base_type; + typedef repeater1<arg1_type, mt_policy> this_type; + + repeater1() { } + repeater1(const this_type& s) : base_type(s) { } + + void reemit(arg1_type a1) { signal1<arg1_type, mt_policy>::emit(a1); } + void repeat(base_type& s) { s.connect(this, &this_type::reemit); } +}; + +template<class arg1_type, class arg2_type, + class mt_policy = SIGSLOT_DEFAULT_MT_POLICY> +class repeater2 : public signal2<arg1_type, arg2_type, mt_policy>, + public has_slots<mt_policy> { + public: + typedef signal2<arg1_type, arg2_type, mt_policy> base_type; + typedef repeater2<arg1_type, arg2_type, mt_policy> this_type; + + repeater2() { } + repeater2(const this_type& s) : base_type(s) { } + + void reemit(arg1_type a1, arg2_type a2) { + signal2<arg1_type, arg2_type, mt_policy>::emit(a1, a2); + } + void repeat(base_type& s) { s.connect(this, &this_type::reemit); } +}; + +template<class arg1_type, class arg2_type, class arg3_type, + class mt_policy = SIGSLOT_DEFAULT_MT_POLICY> +class repeater3 : public signal3<arg1_type, arg2_type, arg3_type, mt_policy>, + public has_slots<mt_policy> { + public: + typedef signal3<arg1_type, arg2_type, arg3_type, mt_policy> base_type; + typedef repeater3<arg1_type, arg2_type, arg3_type, mt_policy> this_type; + + repeater3() { } + repeater3(const this_type& s) : base_type(s) { } + + void reemit(arg1_type a1, arg2_type a2, arg3_type a3) { + signal3<arg1_type, arg2_type, arg3_type, mt_policy>::emit(a1, a2, a3); + } + void repeat(base_type& s) { s.connect(this, &this_type::reemit); } +}; + +} // namespace sigslot + +#endif // JINGLE_NOTIFIER_BASE_SIGSLOTREPEATER_H_ diff --git a/jingle/notifier/base/ssl_adapter.cc b/jingle/notifier/base/ssl_adapter.cc new file mode 100644 index 0000000..2111244 --- /dev/null +++ b/jingle/notifier/base/ssl_adapter.cc @@ -0,0 +1,27 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "jingle/notifier/base/ssl_adapter.h" + +#if defined(OS_WIN) +#include "talk/base/ssladapter.h" +#else +#include "jingle/notifier/communicator/ssl_socket_adapter.h" +#endif + +namespace notifier { + +talk_base::SSLAdapter* CreateSSLAdapter(talk_base::AsyncSocket* socket) { + talk_base::SSLAdapter* ssl_adapter = +#if defined(OS_WIN) + talk_base::SSLAdapter::Create(socket); +#else + notifier::SSLSocketAdapter::Create(socket); +#endif + DCHECK(ssl_adapter); + return ssl_adapter; +} + +} // namespace notifier + diff --git a/jingle/notifier/base/ssl_adapter.h b/jingle/notifier/base/ssl_adapter.h new file mode 100644 index 0000000..32517cd --- /dev/null +++ b/jingle/notifier/base/ssl_adapter.h @@ -0,0 +1,33 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef JINGLE_NOTIFIER_BASE_SSL_ADAPTER_H_ +#define JINGLE_NOTIFIER_BASE_SSL_ADAPTER_H_ + +namespace talk_base { +class AsyncSocket; +class SSLAdapter; +} // namespace talk_base + +namespace notifier { + +// Wraps the given socket in a platform-dependent SSLAdapter +// implementation. +talk_base::SSLAdapter* CreateSSLAdapter(talk_base::AsyncSocket* socket); + +// Utility template class that overrides CreateSSLAdapter() to use the +// above function. +template <class SocketFactory> +class SSLAdapterSocketFactory : public SocketFactory { + public: + virtual talk_base::SSLAdapter* CreateSSLAdapter( + talk_base::AsyncSocket* socket) { + return ::notifier::CreateSSLAdapter(socket); + } +}; + +} // namespace notifier + +#endif // JINGLE_NOTIFIER_BASE_SSL_ADAPTER_H_ + diff --git a/jingle/notifier/base/static_assert.h b/jingle/notifier/base/static_assert.h new file mode 100644 index 0000000..58a8fe4 --- /dev/null +++ b/jingle/notifier/base/static_assert.h @@ -0,0 +1,21 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef JINGLE_NOTIFIER_BASE_STATIC_ASSERT_H_ +#define JINGLE_NOTIFIER_BASE_STATIC_ASSERT_H_ + +template<bool> struct STATIC_ASSERTION_FAILURE; + +template<> struct STATIC_ASSERTION_FAILURE<true> { + enum { value = 1 }; +}; + +template<int> struct static_assert_test{}; + +#define STATIC_ASSERT(B) \ +typedef static_assert_test<\ + sizeof(STATIC_ASSERTION_FAILURE< (bool)( B ) >)>\ + static_assert_typedef_ ## __LINE__ + +#endif // JINGLE_NOTIFIER_BASE_STATIC_ASSERT_H_ diff --git a/jingle/notifier/base/task_pump.cc b/jingle/notifier/base/task_pump.cc new file mode 100644 index 0000000..52ffd26 --- /dev/null +++ b/jingle/notifier/base/task_pump.cc @@ -0,0 +1,45 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "base/message_loop.h" +#include "jingle/notifier/base/task_pump.h" + +namespace notifier { + +TaskPump::TaskPump() + : scoped_runnable_method_factory_( + ALLOW_THIS_IN_INITIALIZER_LIST(this)), + posted_wake_(false) {} + +TaskPump::~TaskPump() {} + +void TaskPump::WakeTasks() { + if (!posted_wake_) { + // Do the requested wake up. + MessageLoop::current()->PostTask( + FROM_HERE, + scoped_runnable_method_factory_.NewRunnableMethod( + &TaskPump::CheckAndRunTasks)); + posted_wake_ = true; + } +} + +int64 TaskPump::CurrentTime() { + // Only timeout tasks rely on this function. Since we're not using + // libjingle tasks for timeout, it's safe to return 0 here. + return 0; +} + +void TaskPump::CheckAndRunTasks() { + posted_wake_ = false; + // We shouldn't be using libjingle for timeout tasks, so we should + // have no timeout tasks at all. + + // TODO(akalin): Add HasTimeoutTask() back in TaskRunner class and + // uncomment this check. + // DCHECK(!HasTimeoutTask()) + RunTasks(); +} + +} // namespace notifier diff --git a/jingle/notifier/base/task_pump.h b/jingle/notifier/base/task_pump.h new file mode 100644 index 0000000..806fed2 --- /dev/null +++ b/jingle/notifier/base/task_pump.h @@ -0,0 +1,34 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef JINGLE_NOTIFIER_BASE_TASK_PUMP_H_ +#define JINGLE_NOTIFIER_BASE_TASK_PUMP_H_ + +#include "base/task.h" +#include "talk/base/taskrunner.h" + +namespace notifier { + +class TaskPump : public talk_base::TaskRunner { + public: + TaskPump(); + + virtual ~TaskPump(); + + // talk_base::TaskRunner implementation. + virtual void WakeTasks(); + virtual int64 CurrentTime(); + + private: + void CheckAndRunTasks(); + + ScopedRunnableMethodFactory<TaskPump> scoped_runnable_method_factory_; + bool posted_wake_; + + DISALLOW_COPY_AND_ASSIGN(TaskPump); +}; + +} // namespace notifier + +#endif // JINGLE_NOTIFIER_BASE_TASK_PUMP_H_ diff --git a/jingle/notifier/communicator/auto_reconnect.cc b/jingle/notifier/communicator/auto_reconnect.cc new file mode 100644 index 0000000..4b16e93 --- /dev/null +++ b/jingle/notifier/communicator/auto_reconnect.cc @@ -0,0 +1,125 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "jingle/notifier/communicator/auto_reconnect.h" + +#include "base/rand_util.h" +#include "jingle/notifier/communicator/login.h" + +namespace notifier { + +const int kResetReconnectInfoDelaySec = 2; + +AutoReconnect::AutoReconnect() + : reconnect_timer_started_(false), + is_idle_(false) { + SetupReconnectInterval(); +} + +void AutoReconnect::NetworkStateChanged(bool is_alive) { + if (is_retrying() && is_alive) { + // Reconnect in 1 to 9 seconds (vary the time a little to try to avoid + // spikey behavior on network hiccups). + StartReconnectTimerWithInterval( + base::TimeDelta::FromSeconds(base::RandInt(1, 9))); + } +} + +void AutoReconnect::StartReconnectTimer() { + StartReconnectTimerWithInterval(reconnect_interval_); +} + +void AutoReconnect::StartReconnectTimerWithInterval( + base::TimeDelta interval) { + // Don't call StopReconnectTimer because we don't want other classes to + // detect that the intermediate state of the timer being stopped. + // (We're avoiding the call to SignalTimerStartStop while reconnect_timer_ is + // NULL). + reconnect_timer_.Stop(); + reconnect_timer_.Start(interval, this, &AutoReconnect::DoReconnect); + reconnect_timer_started_ = true; + SignalTimerStartStop(); +} + +void AutoReconnect::DoReconnect() { + reconnect_timer_started_ = false; + + // If timed out again, double autoreconnect time up to 30 minutes. + reconnect_interval_ *= 2; + if (reconnect_interval_ > base::TimeDelta::FromMinutes(30)) { + reconnect_interval_ = base::TimeDelta::FromMinutes(30); + } + SignalStartConnection(); +} + +void AutoReconnect::StopReconnectTimer() { + if (reconnect_timer_started_) { + reconnect_timer_started_ = false; + reconnect_timer_.Stop(); + SignalTimerStartStop(); + } +} + +void AutoReconnect::StopDelayedResetTimer() { + delayed_reset_timer_.Stop(); +} + +void AutoReconnect::ResetState() { + StopDelayedResetTimer(); + StopReconnectTimer(); + SetupReconnectInterval(); +} + +void AutoReconnect::SetupReconnectInterval() { + if (is_idle_) { + // If we were idle, start the timer over again (120 - 360 seconds). + reconnect_interval_ = + base::TimeDelta::FromSeconds(base::RandInt(120, 360)); + } else { + // If we weren't idle, try the connection 5 - 25 seconds later. + reconnect_interval_ = + base::TimeDelta::FromSeconds(base::RandInt(5, 25)); + } +} + +void AutoReconnect::OnPowerSuspend(bool suspended) { + if (suspended) { + // When the computer comes back on, ensure that the reconnect happens + // quickly (5 - 25 seconds). + reconnect_interval_ = + base::TimeDelta::FromSeconds(base::RandInt(5, 25)); + } +} + +void AutoReconnect::OnClientStateChange(LoginConnectionState state) { + // On any state change, stop the reset timer. + StopDelayedResetTimer(); + switch (state) { + case STATE_RETRYING: + // Do nothing. + break; + + case STATE_CLOSED: + // When the user has been logged out and no auto-reconnect is happening, + // then the autoreconnect intervals should be reset. + ResetState(); + break; + + case STATE_OPENING: + StopReconnectTimer(); + break; + + case STATE_OPENED: + // Reset autoreconnect timeout sequence after being connected for a bit + // of time. This helps in the case that we are connecting briefly and + // then getting disconnect like when an account hits an abuse limit. + StopReconnectTimer(); + delayed_reset_timer_.Start( + base::TimeDelta::FromSeconds(kResetReconnectInfoDelaySec), + this, &AutoReconnect::ResetState); + break; + } +} + +} // namespace notifier diff --git a/jingle/notifier/communicator/auto_reconnect.h b/jingle/notifier/communicator/auto_reconnect.h new file mode 100644 index 0000000..6b2fa09 --- /dev/null +++ b/jingle/notifier/communicator/auto_reconnect.h @@ -0,0 +1,67 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef JINGLE_NOTIFIER_COMMUNICATOR_AUTO_RECONNECT_H_ +#define JINGLE_NOTIFIER_COMMUNICATOR_AUTO_RECONNECT_H_ + +#include <string> + +#include "base/time.h" +#include "base/timer.h" +#include "jingle/notifier/communicator/login_connection_state.h" +#include "talk/base/sigslot.h" + +namespace notifier { + +class AutoReconnect : public sigslot::has_slots<> { + public: + AutoReconnect(); + void StartReconnectTimer(); + void StopReconnectTimer(); + void OnClientStateChange(LoginConnectionState state); + + void NetworkStateChanged(bool is_alive); + + // Callback when power is suspended. + void OnPowerSuspend(bool suspended); + + void set_idle(bool idle) { + is_idle_ = idle; + } + + // Returns true if the auto-retry is to be done (pending a countdown). + bool is_retrying() const { + return reconnect_timer_started_; + } + + sigslot::signal0<> SignalTimerStartStop; + sigslot::signal0<> SignalStartConnection; + + private: + void StartReconnectTimerWithInterval(base::TimeDelta interval); + void DoReconnect(); + void ResetState(); + void SetupReconnectInterval(); + void StopDelayedResetTimer(); + + base::TimeDelta reconnect_interval_; + bool reconnect_timer_started_; + base::OneShotTimer<AutoReconnect> reconnect_timer_; + base::OneShotTimer<AutoReconnect> delayed_reset_timer_; + + bool is_idle_; + DISALLOW_COPY_AND_ASSIGN(AutoReconnect); +}; + +// Wait 2 seconds until after we actually connect to reset reconnect related +// items. +// +// The reason for this delay is to avoid the situation in which buzz is trying +// to block the client due to abuse and the client responses by going into +// rapid reconnect mode, which makes the problem more severe. +extern const int kResetReconnectInfoDelaySec; + +} // namespace notifier + +#endif // JINGLE_NOTIFIER_COMMUNICATOR_AUTO_RECONNECT_H_ diff --git a/jingle/notifier/communicator/connection_options.cc b/jingle/notifier/communicator/connection_options.cc new file mode 100644 index 0000000..fe52d7f --- /dev/null +++ b/jingle/notifier/communicator/connection_options.cc @@ -0,0 +1,17 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "jingle/notifier/communicator/connection_options.h" + +namespace notifier { + +ConnectionOptions::ConnectionOptions() + : autodetect_proxy_(true), + auto_reconnect_(true), + proxy_port_(0), + use_proxy_auth_(0), + allow_unverified_certs_(false) { +} + +} // namespace notifier diff --git a/jingle/notifier/communicator/connection_options.h b/jingle/notifier/communicator/connection_options.h new file mode 100644 index 0000000..de09910 --- /dev/null +++ b/jingle/notifier/communicator/connection_options.h @@ -0,0 +1,56 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef JINGLE_NOTIFIER_COMMUNICATOR_CONNECTION_OPTIONS_H_ +#define JINGLE_NOTIFIER_COMMUNICATOR_CONNECTION_OPTIONS_H_ + +#include <string> + +#include "talk/base/cryptstring.h" +#include "talk/base/helpers.h" + +namespace notifier { + +class ConnectionOptions { + public: + ConnectionOptions(); + + bool autodetect_proxy() const { return autodetect_proxy_; } + bool auto_reconnect() const { return auto_reconnect_; } + const std::string& proxy_host() const { return proxy_host_; } + int proxy_port() const { return proxy_port_; } + bool use_proxy_auth() const { return use_proxy_auth_; } + const std::string& auth_user() const { return auth_user_; } + const talk_base::CryptString& auth_pass() const { return auth_pass_; } + bool allow_unverified_certs() const { return allow_unverified_certs_; } + + void set_autodetect_proxy(bool f) { autodetect_proxy_ = f; } + void set_auto_reconnect(bool f) { auto_reconnect_ = f; } + void set_proxy_host(const std::string& val) { proxy_host_ = val; } + void set_proxy_port(int val) { proxy_port_ = val; } + void set_use_proxy_auth(bool f) { use_proxy_auth_ = f; } + void set_auth_user(const std::string& val) { auth_user_ = val; } + void set_auth_pass(const talk_base::CryptString& val) { auth_pass_ = val; } + + // Setting this to true opens a security hole, so it is *highly* recommended + // that you don't do this. + void set_allow_unverified_certs(bool allow_unverified_certs) { + allow_unverified_certs_ = allow_unverified_certs; + } + + private: + bool autodetect_proxy_; + bool auto_reconnect_; + std::string proxy_host_; + int proxy_port_; + bool use_proxy_auth_; + std::string auth_user_; + talk_base::CryptString auth_pass_; + bool allow_unverified_certs_; + // Allow the copy constructor and operator=. +}; + +} // namespace notifier + +#endif // JINGLE_NOTIFIER_COMMUNICATOR_CONNECTION_OPTIONS_H_ diff --git a/jingle/notifier/communicator/connection_settings.cc b/jingle/notifier/communicator/connection_settings.cc new file mode 100644 index 0000000..1347e46 --- /dev/null +++ b/jingle/notifier/communicator/connection_settings.cc @@ -0,0 +1,128 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <algorithm> +#include <deque> +#include <string> +#include <vector> + +#include "base/logging.h" +#include "jingle/notifier/communicator/connection_settings.h" +#include "talk/base/helpers.h" +#include "talk/xmpp/xmppclientsettings.h" + +namespace notifier { + +class RandomGenerator { + public: + int operator()(int ceiling) { + return static_cast<int>(talk_base::CreateRandomId() % ceiling); + } +}; + +void ConnectionSettings::FillXmppClientSettings( + buzz::XmppClientSettings* xcs) const { + DCHECK(xcs); + xcs->set_protocol(protocol_); + xcs->set_server(server_); + xcs->set_proxy(proxy_.type); + if (proxy_.type != talk_base::PROXY_NONE) { + xcs->set_proxy_host(proxy_.address.IPAsString()); + xcs->set_proxy_port(proxy_.address.port()); + } + if ((proxy_.type != talk_base::PROXY_NONE) && !proxy_.username.empty()) { + xcs->set_use_proxy_auth(true); + xcs->set_proxy_user(proxy_.username); + xcs->set_proxy_pass(proxy_.password); + } else { + xcs->set_use_proxy_auth(false); + } +} + +void ConnectionSettingsList::AddPermutations(const std::string& hostname, + const std::vector<uint32>& iplist, + int16 port, + bool special_port_magic, + bool proxy_only) { + // randomize the list. This ensures the iplist isn't always + // evaluated in the order returned by DNS + std::vector<uint32> iplist_random = iplist; + RandomGenerator rg; + std::random_shuffle(iplist_random.begin(), iplist_random.end(), rg); + + // Put generated addresses in a new deque, then append on the list_, since + // there are order dependencies and AddPermutations() may be called more + // than once. + std::deque<ConnectionSettings> list_temp; + + // Permute addresses for this server. In some cases we haven't resolved the + // to ip addresses. + talk_base::SocketAddress server(hostname, port); + if (iplist_random.empty()) { + // We couldn't pre-resolve the hostname, so let's hope it will resolve + // further down the pipeline (by a proxy, for example). + PermuteForAddress(server, special_port_magic, proxy_only, &list_temp); + } else { + // Generate a set of possibilities for each server address. + // Don't do permute duplicates. + for (size_t index = 0; index < iplist_random.size(); ++index) { + if (std::find(iplist_seen_.begin(), iplist_seen_.end(), + iplist_random[index]) != iplist_seen_.end()) { + continue; + } + iplist_seen_.push_back(iplist_random[index]); + server.SetResolvedIP(iplist_random[index]); + PermuteForAddress(server, special_port_magic, proxy_only, &list_temp); + } + } + + // Add this list to the instance list + while (list_temp.size() != 0) { + list_.push_back(list_temp[0]); + list_temp.pop_front(); + } +} + + +void ConnectionSettingsList::PermuteForAddress( + const talk_base::SocketAddress& server, + bool special_port_magic, + bool proxy_only, + std::deque<ConnectionSettings>* list_temp) { + DCHECK(list_temp); + *(template_.mutable_server()) = server; + + // Use all of the original settings + list_temp->push_back(template_); + + // Try alternate port + if (special_port_magic) { + ConnectionSettings settings(template_); + settings.set_protocol(cricket::PROTO_SSLTCP); + settings.mutable_server()->SetPort(443); + // HTTPS proxies usually require port 443, so try it first + if ((template_.proxy().type == talk_base::PROXY_HTTPS) || + (template_.proxy().type == talk_base::PROXY_UNKNOWN)) { + list_temp->push_front(settings); + } else { + list_temp->push_back(settings); + } + } + + if (!proxy_only) { + // Try without the proxy + if (template_.proxy().type != talk_base::PROXY_NONE) { + ConnectionSettings settings(template_); + settings.mutable_proxy()->type = talk_base::PROXY_NONE; + list_temp->push_back(settings); + + if (special_port_magic) { + settings.set_protocol(cricket::PROTO_SSLTCP); + settings.mutable_server()->SetPort(443); + list_temp->push_back(settings); + } + } + } +} +} // namespace notifier diff --git a/jingle/notifier/communicator/connection_settings.h b/jingle/notifier/communicator/connection_settings.h new file mode 100644 index 0000000..54051a6 --- /dev/null +++ b/jingle/notifier/communicator/connection_settings.h @@ -0,0 +1,76 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef JINGLE_NOTIFIER_COMMUNICATOR_CONNECTION_SETTINGS_H_ +#define JINGLE_NOTIFIER_COMMUNICATOR_CONNECTION_SETTINGS_H_ + +#include <deque> +#include <string> +#include <vector> + +#include "talk/xmpp/xmppclientsettings.h" + +namespace notifier { + +class ConnectionSettings { + public: + ConnectionSettings() : protocol_(cricket::PROTO_TCP) {} + + cricket::ProtocolType protocol() { return protocol_; } + const talk_base::SocketAddress& server() const { return server_; } + const talk_base::ProxyInfo& proxy() const { return proxy_; } + + void set_protocol(cricket::ProtocolType protocol) { protocol_ = protocol; } + talk_base::SocketAddress* mutable_server() { return &server_; } + talk_base::ProxyInfo* mutable_proxy() { return &proxy_; } + + void FillXmppClientSettings(buzz::XmppClientSettings* xcs) const; + + private: + cricket::ProtocolType protocol_; // PROTO_TCP, PROTO_SSLTCP, etc. + talk_base::SocketAddress server_; // Server. + talk_base::ProxyInfo proxy_; // Proxy info. + // Need copy constructor due to use in stl deque. +}; + +class ConnectionSettingsList { + public: + ConnectionSettingsList() {} + + void SetProxy(const talk_base::ProxyInfo& proxy) { + *(template_.mutable_proxy()) = proxy; + } + + const talk_base::ProxyInfo& proxy() const { + return template_.proxy(); + } + + int GetCount() { return list_.size(); } + ConnectionSettings* GetSettings(size_t index) { return &list_[index]; } + + void ClearPermutations() { + list_.clear(); + iplist_seen_.clear(); + } + + void AddPermutations(const std::string& hostname, + const std::vector<uint32>& iplist, + int16 port, + bool special_port_magic, + bool proxy_only); + private: + void PermuteForAddress(const talk_base::SocketAddress& server, + bool special_port_magic, + bool proxy_only, + std::deque<ConnectionSettings>* list_temp); + + ConnectionSettings template_; + std::deque<ConnectionSettings> list_; + std::vector<uint32> iplist_seen_; + DISALLOW_COPY_AND_ASSIGN(ConnectionSettingsList); +}; + +} // namespace notifier + +#endif // JINGLE_NOTIFIER_COMMUNICATOR_CONNECTION_SETTINGS_H_ diff --git a/jingle/notifier/communicator/const_communicator.h b/jingle/notifier/communicator/const_communicator.h new file mode 100644 index 0000000..5f49a62 --- /dev/null +++ b/jingle/notifier/communicator/const_communicator.h @@ -0,0 +1,13 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef JINGLE_NOTIFIER_COMMUNICATOR_CONST_COMMUNICATOR_H_ +#define JINGLE_NOTIFIER_COMMUNICATOR_CONST_COMMUNICATOR_H_ + +namespace notifier { +// The default port for jabber/xmpp communications. +const int kDefaultXmppPort = 5222; +} // namespace notifier + +#endif // JINGLE_NOTIFIER_COMMUNICATOR_CONST_COMMUNICATOR_H_ diff --git a/jingle/notifier/communicator/gaia_token_pre_xmpp_auth.cc b/jingle/notifier/communicator/gaia_token_pre_xmpp_auth.cc new file mode 100644 index 0000000..bcc6f04 --- /dev/null +++ b/jingle/notifier/communicator/gaia_token_pre_xmpp_auth.cc @@ -0,0 +1,76 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "jingle/notifier/communicator/gaia_token_pre_xmpp_auth.h" + +#include <algorithm> + +#include "talk/base/socketaddress.h" +#include "talk/xmpp/constants.h" +#include "talk/xmpp/saslcookiemechanism.h" + +namespace notifier { + +namespace { +const char kGaiaAuthMechanism[] = "X-GOOGLE-TOKEN"; +} // namespace + +GaiaTokenPreXmppAuth::GaiaTokenPreXmppAuth( + const std::string& username, + const std::string& token, + const std::string& token_service) + : username_(username), + token_(token), + token_service_(token_service) { } + +GaiaTokenPreXmppAuth::~GaiaTokenPreXmppAuth() { } + +void GaiaTokenPreXmppAuth::StartPreXmppAuth( + const buzz::Jid& jid, + const talk_base::SocketAddress& server, + const talk_base::CryptString& pass, + const std::string& auth_cookie) { + SignalAuthDone(); +} + +bool GaiaTokenPreXmppAuth::IsAuthDone() const { + return true; +} + +bool GaiaTokenPreXmppAuth::IsAuthorized() const { + return true; +} + +bool GaiaTokenPreXmppAuth::HadError() const { + return false; +} + +int GaiaTokenPreXmppAuth::GetError() const { + return 0; +} + +buzz::CaptchaChallenge GaiaTokenPreXmppAuth::GetCaptchaChallenge() const { + return buzz::CaptchaChallenge(); +} + +std::string GaiaTokenPreXmppAuth::GetAuthCookie() const { + return std::string(); +} + +std::string GaiaTokenPreXmppAuth::ChooseBestSaslMechanism( + const std::vector<std::string> & mechanisms, bool encrypted) { + return (std::find(mechanisms.begin(), + mechanisms.end(), kGaiaAuthMechanism) != + mechanisms.end()) ? kGaiaAuthMechanism : ""; +} + +buzz::SaslMechanism* GaiaTokenPreXmppAuth::CreateSaslMechanism( + const std::string& mechanism) { + if (mechanism != kGaiaAuthMechanism) + return NULL; + return new buzz::SaslCookieMechanism( + kGaiaAuthMechanism, username_, token_, token_service_); +} + +} // namespace notifier diff --git a/jingle/notifier/communicator/gaia_token_pre_xmpp_auth.h b/jingle/notifier/communicator/gaia_token_pre_xmpp_auth.h new file mode 100644 index 0000000..a18c77a --- /dev/null +++ b/jingle/notifier/communicator/gaia_token_pre_xmpp_auth.h @@ -0,0 +1,61 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef JINGLE_NOTIFIER_COMMUNICATOR_GAIA_TOKEN_PRE_XMPP_AUTH_H_ +#define JINGLE_NOTIFIER_COMMUNICATOR_GAIA_TOKEN_PRE_XMPP_AUTH_H_ + +#include <string> +#include <vector> + +#include "talk/xmpp/prexmppauth.h" + +namespace notifier { + +// This class implements buzz::PreXmppAuth interface for token-based +// authentication in GTalk. It looks for the X-GOOGLE-TOKEN auth mechanism +// and uses that instead of the default auth mechanism (PLAIN). +class GaiaTokenPreXmppAuth : public buzz::PreXmppAuth { + public: + GaiaTokenPreXmppAuth(const std::string& username, const std::string& token, + const std::string& token_service); + + virtual ~GaiaTokenPreXmppAuth(); + + // buzz::PreXmppAuth (-buzz::SaslHandler) implementation. We stub + // all the methods out as we don't actually do any authentication at + // this point. + virtual void StartPreXmppAuth(const buzz::Jid& jid, + const talk_base::SocketAddress& server, + const talk_base::CryptString& pass, + const std::string& auth_cookie); + + virtual bool IsAuthDone() const; + + virtual bool IsAuthorized() const; + + virtual bool HadError() const; + + virtual int GetError() const; + + virtual buzz::CaptchaChallenge GetCaptchaChallenge() const; + + virtual std::string GetAuthCookie() const; + + // buzz::SaslHandler implementation. + + virtual std::string ChooseBestSaslMechanism( + const std::vector<std::string>& mechanisms, bool encrypted); + + virtual buzz::SaslMechanism* CreateSaslMechanism( + const std::string& mechanism); + + private: + std::string username_; + std::string token_; + std::string token_service_; +}; + +} // namespace notifier + +#endif // JINGLE_NOTIFIER_COMMUNICATOR_GAIA_TOKEN_PRE_XMPP_AUTH_H_ diff --git a/jingle/notifier/communicator/login.cc b/jingle/notifier/communicator/login.cc new file mode 100644 index 0000000..74af528 --- /dev/null +++ b/jingle/notifier/communicator/login.cc @@ -0,0 +1,344 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <string> + +#include "jingle/notifier/communicator/login.h" + +#include "base/logging.h" +#include "base/time.h" +#include "jingle/notifier/communicator/auto_reconnect.h" +#include "jingle/notifier/communicator/connection_options.h" +#include "jingle/notifier/communicator/login_settings.h" +#include "jingle/notifier/communicator/product_info.h" +#include "jingle/notifier/communicator/single_login_attempt.h" +#include "net/base/host_port_pair.h" +#include "talk/base/common.h" +#include "talk/base/firewallsocketserver.h" +#include "talk/base/logging.h" +#include "talk/base/physicalsocketserver.h" +#include "talk/base/taskrunner.h" +#include "talk/xmllite/xmlelement.h" +#include "talk/xmpp/asyncsocket.h" +#include "talk/xmpp/prexmppauth.h" +#include "talk/xmpp/xmppclient.h" +#include "talk/xmpp/xmppclientsettings.h" +#include "talk/xmpp/xmppengine.h" + +namespace notifier { + +// Redirect valid for 5 minutes. +static const int kRedirectTimeoutMinutes = 5; + +// Disconnect if network stays down for more than 10 seconds. +static const int kDisconnectionDelaySecs = 10; + +Login::Login(talk_base::TaskParent* parent, + const buzz::XmppClientSettings& user_settings, + const ConnectionOptions& options, + std::string lang, + net::HostResolver* host_resolver, + ServerInformation* server_list, + int server_count, + talk_base::FirewallManager* firewall, + bool proxy_only, + bool previous_login_successful) + : parent_(parent), + login_settings_(new LoginSettings(user_settings, + options, + lang, + host_resolver, + server_list, + server_count, + firewall, + proxy_only)), + single_attempt_(NULL), + successful_connection_(previous_login_successful), + state_(STATE_OPENING), + redirect_port_(0), + unexpected_disconnect_occurred_(false), + google_host_(user_settings.host()), + google_user_(user_settings.user()) { + // Hook up all the signals and observers. + net::NetworkChangeNotifier::AddObserver(this); + auto_reconnect_.SignalStartConnection.connect(this, + &Login::StartConnection); + auto_reconnect_.SignalTimerStartStop.connect( + this, + &Login::OnAutoReconnectTimerChange); + SignalClientStateChange.connect(&auto_reconnect_, + &AutoReconnect::OnClientStateChange); + SignalIdleChange.connect(&auto_reconnect_, + &AutoReconnect::set_idle); + SignalPowerSuspended.connect(&auto_reconnect_, + &AutoReconnect::OnPowerSuspend); + + // Then check the initial state of the connection. + CheckConnection(); +} + +// Defined so that the destructors are executed here (and the corresponding +// classes don't need to be included in the header file). +Login::~Login() { + if (single_attempt_) { + single_attempt_->Abort(); + single_attempt_ = NULL; + } + net::NetworkChangeNotifier::RemoveObserver(this); +} + +void Login::StartConnection() { + // If there is a server redirect, use it. + if (base::Time::Now() < + (redirect_time_ + + base::TimeDelta::FromMinutes(kRedirectTimeoutMinutes))) { + // Override server/port with redirect values. + DCHECK_NE(redirect_port_, 0); + net::HostPortPair server_override(redirect_server_, redirect_port_); + login_settings_->set_server_override(server_override); + } else { + login_settings_->clear_server_override(); + } + + if (single_attempt_) { + single_attempt_->Abort(); + single_attempt_ = NULL; + } + single_attempt_ = new SingleLoginAttempt(parent_, + login_settings_.get(), + successful_connection_); + + // Do the signaling hook-ups. + single_attempt_->SignalLoginFailure.connect(this, &Login::OnLoginFailure); + single_attempt_->SignalRedirect.connect(this, &Login::OnRedirect); + single_attempt_->SignalClientStateChange.connect( + this, + &Login::OnClientStateChange); + single_attempt_->SignalUnexpectedDisconnect.connect( + this, + &Login::OnUnexpectedDisconnect); + single_attempt_->SignalLogoff.connect( + this, + &Login::OnLogoff); + single_attempt_->SignalNeedAutoReconnect.connect( + this, + &Login::DoAutoReconnect); + SignalLogInput.repeat(single_attempt_->SignalLogInput); + SignalLogOutput.repeat(single_attempt_->SignalLogOutput); + + single_attempt_->Start(); +} + +const std::string& Login::google_host() const { + return google_host_; +} + +const std::string& Login::google_user() const { + return google_user_; +} + +const talk_base::ProxyInfo& Login::proxy() const { + return proxy_info_; +} + +void Login::OnLoginFailure(const LoginFailure& failure) { + auto_reconnect_.StopReconnectTimer(); + HandleClientStateChange(STATE_CLOSED); + SignalLoginFailure(failure); +} + +void Login::OnLogoff() { + HandleClientStateChange(STATE_CLOSED); +} + +void Login::OnClientStateChange(buzz::XmppEngine::State state) { + LoginConnectionState new_state = STATE_CLOSED; + + switch (state) { + case buzz::XmppEngine::STATE_NONE: + case buzz::XmppEngine::STATE_CLOSED: + // Ignore the closed state (because we may be trying the next dns entry). + // + // But we go to this state for other signals when there is no retry + // happening. + new_state = state_; + break; + + case buzz::XmppEngine::STATE_START: + case buzz::XmppEngine::STATE_OPENING: + new_state = STATE_OPENING; + break; + + case buzz::XmppEngine::STATE_OPEN: + new_state = STATE_OPENED; + break; + + default: + DCHECK(false); + break; + } + HandleClientStateChange(new_state); +} + +void Login::HandleClientStateChange(LoginConnectionState new_state) { + // Do we need to transition between the retrying and closed states? + if (auto_reconnect_.is_retrying()) { + if (new_state == STATE_CLOSED) { + new_state = STATE_RETRYING; + } + } else { + if (new_state == STATE_RETRYING) { + new_state = STATE_CLOSED; + } + } + + if (new_state != state_) { + state_ = new_state; + reset_unexpected_timer_.Stop(); + + if (state_ == STATE_OPENED) { + successful_connection_ = true; + + google_host_ = single_attempt_->xmpp_client()->jid().domain(); + google_user_ = single_attempt_->xmpp_client()->jid().node(); + proxy_info_ = single_attempt_->proxy(); + + reset_unexpected_timer_.Start( + base::TimeDelta::FromSeconds(kResetReconnectInfoDelaySec), + this, &Login::ResetUnexpectedDisconnect); + } + SignalClientStateChange(state_); + } +} + +void Login::OnAutoReconnectTimerChange() { + if (!single_attempt_ || !single_attempt_->xmpp_client()) { + HandleClientStateChange(STATE_CLOSED); + return; + } + OnClientStateChange(single_attempt_->xmpp_client()->GetState()); +} + +buzz::XmppClient* Login::xmpp_client() { + if (!single_attempt_) { + return NULL; + } + return single_attempt_->xmpp_client(); +} + +void Login::UseNextConnection() { + if (!single_attempt_) { + // Just in case, there is an obscure case that causes this to get called + // when there is no single_attempt_. + return; + } + single_attempt_->UseNextConnection(); +} + +void Login::UseCurrentConnection() { + if (!single_attempt_) { + // Just in case, there is an obscure case that causes this to get called + // when there is no single_attempt_. + return; + } + single_attempt_->UseCurrentConnection(); +} + +void Login::OnRedirect(const std::string& redirect_server, int redirect_port) { + DCHECK_NE(redirect_port_, 0); + + redirect_time_ = base::Time::Now(); + redirect_server_ = redirect_server; + redirect_port_ = redirect_port; + + // Drop the current connection, and start the login process again. + StartConnection(); +} + +void Login::OnUnexpectedDisconnect() { + reset_unexpected_timer_.Stop(); + + // Start the login process again. + if (unexpected_disconnect_occurred_) { + // If we already have received an unexpected disconnect recently, then our + // account may have be jailed due to abuse, so we shouldn't make the + // situation worse by trying really hard to reconnect. Instead, we'll do + // the autoreconnect route, which has exponential back-off. + DoAutoReconnect(); + return; + } + StartConnection(); + unexpected_disconnect_occurred_ = true; +} + +void Login::ResetUnexpectedDisconnect() { + unexpected_disconnect_occurred_ = false; +} + +void Login::DoAutoReconnect() { + bool allow_auto_reconnect = + login_settings_->connection_options().auto_reconnect(); + // Start the reconnect time before aborting the connection to ensure that + // AutoReconnect::is_retrying() is true, so that the Login doesn't + // transition to the CLOSED state (which would cause the reconnection timer + // to reset and not double). + if (allow_auto_reconnect) { + auto_reconnect_.StartReconnectTimer(); + } + + if (single_attempt_) { + single_attempt_->Abort(); + single_attempt_ = NULL; + } + + if (!allow_auto_reconnect) { + HandleClientStateChange(STATE_CLOSED); + return; + } +} + +void Login::OnIPAddressChanged() { + LOG(INFO) << "IP address change detected"; + CheckConnection(); +} + +void Login::CheckConnection() { + LOG(INFO) << "Checking connection"; + talk_base::PhysicalSocketServer physical; + scoped_ptr<talk_base::Socket> socket(physical.CreateSocket(SOCK_STREAM)); + bool alive = + !socket->Connect(talk_base::SocketAddress("talk.google.com", 5222)); + LOG(INFO) << "Network is " << (alive ? "alive" : "not alive"); + if (alive) { + // Our connection is up. If we have a disconnect timer going, + // stop it so we don't disconnect. + disconnect_timer_.Stop(); + } else { + // Our network connection is down. Start the disconnect timer if + // it's not already going. Don't disconnect immediately to avoid + // constant connection/disconnection due to flaky network + // interfaces. + if (!disconnect_timer_.IsRunning()) { + disconnect_timer_.Start( + base::TimeDelta::FromSeconds(kDisconnectionDelaySecs), + this, &Login::OnDisconnectTimeout); + } + } + auto_reconnect_.NetworkStateChanged(alive); +} + +void Login::OnDisconnectTimeout() { + if (state_ != STATE_OPENED) { + return; + } + + if (single_attempt_) { + single_attempt_->Abort(); + single_attempt_ = NULL; + } + + StartConnection(); +} + +} // namespace notifier diff --git a/jingle/notifier/communicator/login.h b/jingle/notifier/communicator/login.h new file mode 100644 index 0000000..bd41cb5 --- /dev/null +++ b/jingle/notifier/communicator/login.h @@ -0,0 +1,151 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef JINGLE_NOTIFIER_COMMUNICATOR_LOGIN_H_ +#define JINGLE_NOTIFIER_COMMUNICATOR_LOGIN_H_ + +#include <string> + +#include "base/time.h" +#include "base/timer.h" +#include "jingle/notifier/base/sigslotrepeater.h" +#include "jingle/notifier/communicator/auto_reconnect.h" +#include "jingle/notifier/communicator/login_connection_state.h" +#include "net/base/network_change_notifier.h" +#include "talk/base/proxyinfo.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/sigslot.h" +#include "talk/xmpp/xmppengine.h" + +namespace buzz { +class XmppClient; +class XmppEngine; +class XmppClientSettings; +} // namespace buzz + +namespace net { +class HostResolver; +} // namespace net + +namespace talk_base { +class FirewallManager; +struct ProxyInfo; +class TaskParent; +} // namespace talk_base + +namespace notifier { + +class ConnectionOptions; +class LoginFailure; +class LoginSettings; +struct ServerInformation; +class SingleLoginAttempt; + +// Does the login, keeps it alive (with refreshing cookies and reattempting +// login when disconnected), figures out what actions to take on the various +// errors that may occur. +class Login : public net::NetworkChangeNotifier::Observer, + public sigslot::has_slots<> { + public: + // network_status and firewall may be NULL. + Login(talk_base::TaskParent* parent, + const buzz::XmppClientSettings& user_settings, + const ConnectionOptions& options, + std::string lang, + net::HostResolver* host_resolver, + ServerInformation* server_list, + int server_count, + talk_base::FirewallManager* firewall, + bool proxy_only, + bool previous_login_successful); + virtual ~Login(); + + LoginConnectionState connection_state() const { + return state_; + } + + void StartConnection(); + void UseNextConnection(); + void UseCurrentConnection(); + buzz::XmppClient* xmpp_client(); + + // Start the auto-reconnect. It may not do the auto-reconnect if + // auto-reconnect is turned off. + void DoAutoReconnect(); + + const LoginSettings& login_settings() const { + return *(login_settings_.get()); + } + + // Returns the best guess at the host responsible for the account (which we + // use to determine if it is a dasher account or not). + // + // After login this may return a more accurate answer, which accounts for + // open sign-up accounts. + const std::string& google_host() const; + + // Analogous to google_host but for the user account ("fred" in + // "fred@gmail.com"). + const std::string& google_user() const; + + // Returns the proxy that is being used to connect (or the default proxy + // information if all attempted connections failed). + // + // Do not call until StartConnection has been called. + const talk_base::ProxyInfo& proxy() const; + + virtual void OnIPAddressChanged(); + + sigslot::signal1<LoginConnectionState> SignalClientStateChange; + + sigslot::signal1<const LoginFailure&> SignalLoginFailure; + sigslot::repeater2<const char*, int> SignalLogInput; + sigslot::repeater2<const char*, int> SignalLogOutput; + sigslot::repeater1<bool> SignalIdleChange; + + // The creator should hook this up to a signal that indicates when the power + // is being suspended. + sigslot::repeater1<bool> SignalPowerSuspended; + + private: + void CheckConnection(); + + void OnRedirect(const std::string& redirect_server, int redirect_port); + void OnUnexpectedDisconnect(); + void OnClientStateChange(buzz::XmppEngine::State state); + void OnLoginFailure(const LoginFailure& failure); + void OnLogoff(); + void OnAutoReconnectTimerChange(); + + void HandleClientStateChange(LoginConnectionState new_state); + void ResetUnexpectedDisconnect(); + + void OnDisconnectTimeout(); + + talk_base::TaskParent* parent_; + scoped_ptr<LoginSettings> login_settings_; + AutoReconnect auto_reconnect_; + SingleLoginAttempt* single_attempt_; + bool successful_connection_; + + LoginConnectionState state_; + + // server redirect information + base::Time redirect_time_; + std::string redirect_server_; + int redirect_port_; + bool unexpected_disconnect_occurred_; + base::OneShotTimer<Login> reset_unexpected_timer_; + std::string google_host_; + std::string google_user_; + talk_base::ProxyInfo proxy_info_; + + base::OneShotTimer<Login> disconnect_timer_; + + DISALLOW_COPY_AND_ASSIGN(Login); +}; + +} // namespace notifier + +#endif // JINGLE_NOTIFIER_COMMUNICATOR_LOGIN_H_ diff --git a/jingle/notifier/communicator/login_connection_state.h b/jingle/notifier/communicator/login_connection_state.h new file mode 100644 index 0000000..db90451 --- /dev/null +++ b/jingle/notifier/communicator/login_connection_state.h @@ -0,0 +1,24 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// LoginConnectionState is an enum representing the state of the XMPP +// connection. + +#ifndef JINGLE_NOTIFIER_COMMUNICATOR_LOGIN_CONNECTION_STATE_H_ +#define JINGLE_NOTIFIER_COMMUNICATOR_LOGIN_CONNECTION_STATE_H_ + +namespace notifier { + +enum LoginConnectionState { + STATE_CLOSED, + // Same as the closed state but indicates that a countdown is + // happening for auto-retrying the connection. + STATE_RETRYING, + STATE_OPENING, + STATE_OPENED, +}; + +} // namespace notifier + +#endif // JINGLE_NOTIFIER_COMMUNICATOR_LOGIN_CONNECTION_STATE_H_ diff --git a/jingle/notifier/communicator/login_failure.cc b/jingle/notifier/communicator/login_failure.cc new file mode 100644 index 0000000..85b0fcf --- /dev/null +++ b/jingle/notifier/communicator/login_failure.cc @@ -0,0 +1,30 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "jingle/notifier/communicator/login_failure.h" + +#include "base/logging.h" + +namespace notifier { + +LoginFailure::LoginFailure(LoginError error) + : error_(error), + xmpp_error_(buzz::XmppEngine::ERROR_NONE), + subcode_(0) { +} + +LoginFailure::LoginFailure(LoginError error, + buzz::XmppEngine::Error xmpp_error, + int subcode) + : error_(error), + xmpp_error_(xmpp_error), + subcode_(subcode) { +} + +buzz::XmppEngine::Error LoginFailure::xmpp_error() const { + DCHECK_EQ(error_, XMPP_ERROR); + return xmpp_error_; +} + +} // namespace notifier diff --git a/jingle/notifier/communicator/login_failure.h b/jingle/notifier/communicator/login_failure.h new file mode 100644 index 0000000..af4b66c --- /dev/null +++ b/jingle/notifier/communicator/login_failure.h @@ -0,0 +1,50 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef JINGLE_NOTIFIER_COMMUNICATOR_LOGIN_FAILURE_H_ +#define JINGLE_NOTIFIER_COMMUNICATOR_LOGIN_FAILURE_H_ + +#include "talk/xmpp/xmppengine.h" + +namespace notifier { + +class LoginFailure { + public: + enum LoginError { + // Check the xmpp_error for more information. + XMPP_ERROR, + + // If the certificate has expired, it usually means that the computer's + // clock isn't set correctly. + CERTIFICATE_EXPIRED_ERROR, + + // Apparently, there is a proxy that needs authentication information. + PROXY_AUTHENTICATION_ERROR, + }; + + explicit LoginFailure(LoginError error); + LoginFailure(LoginError error, + buzz::XmppEngine::Error xmpp_error, + int subcode); + + // Used as the first level of error information. + LoginError error() const { + return error_; + } + + // Returns the XmppEngine only. Valid if and only if error() == XMPP_ERROR. + // + // Handler should mimic logic from PhoneWindow::ShowConnectionError + // (except that the DiagnoseConnectionError has already been done). + buzz::XmppEngine::Error xmpp_error() const; + + private: + const LoginError error_; + const buzz::XmppEngine::Error xmpp_error_; + const int subcode_; +}; + +} // namespace notifier + +#endif // JINGLE_NOTIFIER_COMMUNICATOR_LOGIN_FAILURE_H_ diff --git a/jingle/notifier/communicator/login_settings.cc b/jingle/notifier/communicator/login_settings.cc new file mode 100644 index 0000000..43298eb --- /dev/null +++ b/jingle/notifier/communicator/login_settings.cc @@ -0,0 +1,59 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <string> + +#include "jingle/notifier/communicator/login_settings.h" + +#include "base/logging.h" +#include "jingle/notifier/communicator/connection_options.h" +#include "jingle/notifier/communicator/xmpp_connection_generator.h" +#include "talk/base/common.h" +#include "talk/base/socketaddress.h" +#include "talk/xmpp/xmppclientsettings.h" + +namespace notifier { + +LoginSettings::LoginSettings(const buzz::XmppClientSettings& user_settings, + const ConnectionOptions& options, + const std::string& lang, + net::HostResolver* host_resolver, + ServerInformation* server_list, + int server_count, + talk_base::FirewallManager* firewall, + bool proxy_only) + : proxy_only_(proxy_only), + firewall_(firewall), + lang_(lang), + host_resolver_(host_resolver), + server_list_(new ServerInformation[server_count]), + server_count_(server_count), + user_settings_(new buzz::XmppClientSettings(user_settings)), + connection_options_(new ConnectionOptions(options)) { + // Note: firewall may be NULL. + DCHECK(server_list); + DCHECK(host_resolver); + DCHECK_GT(server_count, 0); + for (int i = 0; i < server_count_; ++i) { + server_list_[i] = server_list[i]; + } +} + +// Defined so that the destructors are executed here (and the corresponding +// classes don't need to be included in the header file). +LoginSettings::~LoginSettings() { +} + +void LoginSettings::set_server_override( + const net::HostPortPair& server) { + server_override_.reset(new ServerInformation()); + server_override_->server = server; + server_override_->special_port_magic = server_list_[0].special_port_magic; +} + +void LoginSettings::clear_server_override() { + server_override_.reset(); +} + +} // namespace notifier diff --git a/jingle/notifier/communicator/login_settings.h b/jingle/notifier/communicator/login_settings.h new file mode 100644 index 0000000..32ea038 --- /dev/null +++ b/jingle/notifier/communicator/login_settings.h @@ -0,0 +1,102 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef JINGLE_NOTIFIER_COMMUNICATOR_LOGIN_SETTINGS_H_ +#define JINGLE_NOTIFIER_COMMUNICATOR_LOGIN_SETTINGS_H_ +#include <string> + +#include "jingle/notifier/communicator/xmpp_connection_generator.h" +#include "talk/base/scoped_ptr.h" + +namespace buzz { +class XmppClientSettings; +} + +namespace net { +struct HostPortPair; +class HostResolver; +} + +namespace talk_base { +class FirewallManager; +class SocketAddress; +} + +namespace notifier { +class ConnectionOptions; +struct ServerInformation; + +class LoginSettings { + public: + LoginSettings(const buzz::XmppClientSettings& user_settings, + const ConnectionOptions& options, + const std::string& lang, + net::HostResolver* host_resolver, + ServerInformation* server_list, + int server_count, + talk_base::FirewallManager* firewall, + bool proxy_only); + + ~LoginSettings(); + + // Note: firewall() may return NULL. + // + // Could be a const method, but it allows + // modification of part (FirewallManager) of its state. + talk_base::FirewallManager* firewall() { + return firewall_; + } + + bool proxy_only() const { + return proxy_only_; + } + + const std::string& lang() const { + return lang_; + } + + net::HostResolver* host_resolver() { + return host_resolver_; + } + + const ServerInformation* server_list() const { + return server_override_.get() ? server_override_.get() : server_list_.get(); + } + + int server_count() const { + return server_override_.get() ? 1 : server_count_; + } + + const buzz::XmppClientSettings& user_settings() const { + return *user_settings_.get(); + } + + buzz::XmppClientSettings* modifiable_user_settings() { + return user_settings_.get(); + } + + const ConnectionOptions& connection_options() const { + return *connection_options_.get(); + } + + void set_server_override(const net::HostPortPair& server); + void clear_server_override(); + + private: + bool proxy_only_; + talk_base::FirewallManager* firewall_; + std::string lang_; + + net::HostResolver* host_resolver_; + talk_base::scoped_array<ServerInformation> server_list_; + int server_count_; + // Used to handle redirects + scoped_ptr<ServerInformation> server_override_; + + scoped_ptr<buzz::XmppClientSettings> user_settings_; + scoped_ptr<ConnectionOptions> connection_options_; + DISALLOW_COPY_AND_ASSIGN(LoginSettings); +}; +} // namespace notifier +#endif // JINGLE_NOTIFIER_COMMUNICATOR_LOGIN_SETTINGS_H_ diff --git a/jingle/notifier/communicator/product_info.cc b/jingle/notifier/communicator/product_info.cc new file mode 100644 index 0000000..c1deafb --- /dev/null +++ b/jingle/notifier/communicator/product_info.cc @@ -0,0 +1,15 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <string> + +namespace notifier { +std::string GetUserAgentString() { + return kXmppProductName; +} + +std::string GetProductSignature() { + return kXmppProductName; +} +} // namespace notifier diff --git a/jingle/notifier/communicator/product_info.h b/jingle/notifier/communicator/product_info.h new file mode 100644 index 0000000..9e8e5d0 --- /dev/null +++ b/jingle/notifier/communicator/product_info.h @@ -0,0 +1,15 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef JINGLE_NOTIFIER_COMMUNICATOR_PRODUCT_INFO_H_ +#define JINGLE_NOTIFIER_COMMUNICATOR_PRODUCT_INFO_H_ + +#include <string> + +namespace notifier { +std::string GetUserAgentString(); +std::string GetProductSignature(); +} // namespace notifier + +#endif // JINGLE_NOTIFIER_COMMUNICATOR_PRODUCT_INFO_H_ diff --git a/jingle/notifier/communicator/single_login_attempt.cc b/jingle/notifier/communicator/single_login_attempt.cc new file mode 100644 index 0000000..00163c09 --- /dev/null +++ b/jingle/notifier/communicator/single_login_attempt.cc @@ -0,0 +1,517 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <algorithm> +#include <string> +#include <vector> + +#include "jingle/notifier/communicator/single_login_attempt.h" + +#include "base/logging.h" +#include "jingle/notifier/communicator/connection_options.h" +#include "jingle/notifier/communicator/connection_settings.h" +#include "jingle/notifier/communicator/const_communicator.h" +#include "jingle/notifier/communicator/gaia_token_pre_xmpp_auth.h" +#include "jingle/notifier/communicator/login_failure.h" +#include "jingle/notifier/communicator/login_settings.h" +#include "jingle/notifier/communicator/product_info.h" +#include "jingle/notifier/communicator/xmpp_connection_generator.h" +#include "jingle/notifier/communicator/xmpp_socket_adapter.h" +#include "talk/base/asynchttprequest.h" +#include "talk/base/firewallsocketserver.h" +#include "talk/base/signalthread.h" +#include "talk/base/taskrunner.h" +#include "talk/base/win32socketinit.h" +#include "talk/xmllite/xmlelement.h" +#include "talk/xmpp/xmppclient.h" +#include "talk/xmpp/xmppclientsettings.h" +#include "talk/xmpp/constants.h" + +namespace notifier { + +static void GetClientErrorInformation( + buzz::XmppClient* client, + buzz::XmppEngine::Error* error, + int* subcode, + buzz::XmlElement** stream_error) { + DCHECK(client); + DCHECK(error); + DCHECK(subcode); + DCHECK(stream_error); + + *error = client->GetError(subcode); + + *stream_error = NULL; + if (*error == buzz::XmppEngine::ERROR_STREAM) { + const buzz::XmlElement* error_element = client->GetStreamError(); + if (error_element) { + *stream_error = new buzz::XmlElement(*error_element); + } + } +} + +SingleLoginAttempt::SingleLoginAttempt(talk_base::TaskParent* parent, + LoginSettings* login_settings, + bool successful_connection) + : talk_base::Task(parent), + state_(buzz::XmppEngine::STATE_NONE), + code_(buzz::XmppEngine::ERROR_NONE), + subcode_(0), + need_authentication_(false), + certificate_expired_(false), + cookie_refreshed_(false), + successful_connection_(successful_connection), + login_settings_(login_settings), + client_(NULL) { +#if defined(OS_WIN) + talk_base::EnsureWinsockInit(); +#endif + connection_generator_.reset(new XmppConnectionGenerator( + this, + login_settings_->host_resolver(), + &login_settings_->connection_options(), + login_settings_->proxy_only(), + login_settings_->server_list(), + login_settings_->server_count())); + + connection_generator_->SignalExhaustedSettings.connect( + this, + &SingleLoginAttempt::OnAttemptedAllConnections); + connection_generator_->SignalNewSettings.connect( + this, + &SingleLoginAttempt::DoLogin); +} + +SingleLoginAttempt::~SingleLoginAttempt() { + // If this assertion goes off, it means that "Stop()" didn't get called like + // it should have been. + DCHECK(!client_); +} + +bool SingleLoginAttempt::auto_reconnect() const { + return login_settings_->connection_options().auto_reconnect(); +} + +const talk_base::ProxyInfo& SingleLoginAttempt::proxy() const { + DCHECK(connection_generator_.get()); + return connection_generator_->proxy(); +} + +int SingleLoginAttempt::ProcessStart() { + DCHECK_EQ(GetState(), talk_base::Task::STATE_START); + connection_generator_->StartGenerating(); + + // After being started, this class is callback driven and does signaling from + // those callbacks (with checks to see if it is done if it may be called back + // from something that isn't a child task). + return talk_base::Task::STATE_BLOCKED; +} + +void SingleLoginAttempt::Stop() { + ClearClient(); + talk_base::Task::Stop(); + + // No more signals should happen after being stopped. This is needed because + // some of these signals happen due to other components doing signaling which + // may continue running even though this task is stopped. + SignalUnexpectedDisconnect.disconnect_all(); + SignalRedirect.disconnect_all(); + SignalLoginFailure.disconnect_all(); + SignalNeedAutoReconnect.disconnect_all(); + SignalClientStateChange.disconnect_all(); +} + +void SingleLoginAttempt::OnAttemptedAllConnections( + bool successfully_resolved_dns, + int first_dns_error) { + + // Maybe we needed proxy authentication? + if (need_authentication_) { + LoginFailure failure(LoginFailure::PROXY_AUTHENTICATION_ERROR); + SignalLoginFailure(failure); + return; + } + + if (certificate_expired_) { + LoginFailure failure(LoginFailure::CERTIFICATE_EXPIRED_ERROR); + SignalLoginFailure(failure); + return; + } + + if (!successfully_resolved_dns) { + code_ = buzz::XmppEngine::ERROR_SOCKET; + subcode_ = first_dns_error; + } + + LOG(INFO) << "Connection failed with error " << code_; + + // We were connected and we had a problem. + if (successful_connection_ && auto_reconnect()) { + SignalNeedAutoReconnect(); + // Expect to be deleted at this point. + return; + } + + DiagnoseConnectionError(); +} + +void SingleLoginAttempt::UseNextConnection() { + DCHECK(connection_generator_.get()); + ClearClient(); + connection_generator_->UseNextConnection(); +} + +void SingleLoginAttempt::UseCurrentConnection() { + DCHECK(connection_generator_.get()); + ClearClient(); + connection_generator_->UseCurrentConnection(); +} + +void SingleLoginAttempt::DoLogin( + const ConnectionSettings& connection_settings) { + if (client_) { + return; + } + + buzz::XmppClientSettings client_settings; + // Set the user settings portion. + *static_cast<buzz::XmppClientSettings*>(&client_settings) = + login_settings_->user_settings(); + // Fill in the rest of the client settings. + connection_settings.FillXmppClientSettings(&client_settings); + + client_ = new buzz::XmppClient(this); + SignalLogInput.repeat(client_->SignalLogInput); + SignalLogOutput.repeat(client_->SignalLogOutput); + + // Listen for connection progress. + client_->SignalStateChange.connect(this, + &SingleLoginAttempt::OnClientStateChange); + + // Transition to "start". + OnClientStateChange(buzz::XmppEngine::STATE_START); + // Start connecting. + client_->Connect(client_settings, login_settings_->lang(), + CreateSocket(client_settings), + CreatePreXmppAuth(client_settings)); + client_->Start(); +} + +void SingleLoginAttempt::OnAuthenticationError() { + // We can check this flag later if all connection options fail. + need_authentication_ = true; +} + +void SingleLoginAttempt::OnCertificateExpired() { + // We can check this flag later if all connection options fail. + certificate_expired_ = true; +} + +buzz::AsyncSocket* SingleLoginAttempt::CreateSocket( + const buzz::XmppClientSettings& xcs) { + bool allow_unverified_certs = + login_settings_->connection_options().allow_unverified_certs(); + XmppSocketAdapter* adapter = new XmppSocketAdapter(xcs, + allow_unverified_certs); + adapter->SignalAuthenticationError.connect( + this, + &SingleLoginAttempt::OnAuthenticationError); + if (login_settings_->firewall()) { + adapter->set_firewall(true); + } + return adapter; +} + +buzz::PreXmppAuth* SingleLoginAttempt::CreatePreXmppAuth( + const buzz::XmppClientSettings& xcs) { + buzz::Jid jid(xcs.user(), xcs.host(), buzz::STR_EMPTY); + return new GaiaTokenPreXmppAuth( + jid.Str(), xcs.auth_cookie(), xcs.token_service()); +} + +void SingleLoginAttempt::OnFreshAuthCookie(const std::string& auth_cookie) { + // Remember this is a fresh cookie. + cookie_refreshed_ = true; + + // TODO(sync): do the cookie logic (part of which is in the #if 0 below). + + // The following code is what PhoneWindow does for the equivalent method. +#if 0 + // Save cookie + AccountInfo current(account_history_.current()); + current.set_auth_cookie(auth_cookie); + account_history_.set_current(current); + + // Calc next time to refresh cookie, between 5 and 10 days. The cookie has + // 14 days of life; this gives at least 4 days of retries before the current + // cookie expires, maximizing the chance of having a valid cookie next time + // the connection servers go down. + FTULL now; + + // NOTE: The following line is win32. Address this when implementing this + // code (doing "the cookie logic"). + GetSystemTimeAsFileTime(&(now.ft)); + ULONGLONG five_days = (ULONGLONG)10000 * 1000 * 60 * 60 * 24 * 5; // 5 days + ULONGLONG random = (ULONGLONG)10000 * // get to 100 ns units + ((rand() % (5 * 24 * 60)) * (60 * 1000) + // random min. in 5 day period + (rand() % 1000) * 60); // random 1/1000th of a minute + next_cookie_refresh_ = now.ull + five_days + random; // 5-10 days +#endif +} + +void SingleLoginAttempt::DiagnoseConnectionError() { + switch (code_) { + case buzz::XmppEngine::ERROR_MISSING_USERNAME: + case buzz::XmppEngine::ERROR_NETWORK_TIMEOUT: + case buzz::XmppEngine::ERROR_DOCUMENT_CLOSED: + case buzz::XmppEngine::ERROR_BIND: + case buzz::XmppEngine::ERROR_AUTH: + case buzz::XmppEngine::ERROR_TLS: + case buzz::XmppEngine::ERROR_UNAUTHORIZED: + case buzz::XmppEngine::ERROR_VERSION: + case buzz::XmppEngine::ERROR_STREAM: + case buzz::XmppEngine::ERROR_XML: + case buzz::XmppEngine::ERROR_NONE: + default: { + LoginFailure failure(LoginFailure::XMPP_ERROR, code_, subcode_); + SignalLoginFailure(failure); + return; + } + + // The following errors require diagnosistics: + // * spurious close of connection + // * socket errors after auth + case buzz::XmppEngine::ERROR_CONNECTION_CLOSED: + case buzz::XmppEngine::ERROR_SOCKET: + break; + } + + talk_base::AsyncHttpRequest *http_request = + new talk_base::AsyncHttpRequest(GetUserAgentString()); + http_request->set_host("www.google.com"); + http_request->set_port(80); + http_request->set_secure(false); + http_request->request().path = "/"; + http_request->request().verb = talk_base::HV_GET; + + talk_base::ProxyInfo proxy; + DCHECK(connection_generator_.get()); + if (connection_generator_.get()) { + proxy = connection_generator_->proxy(); + } + http_request->set_proxy(proxy); + http_request->set_firewall(login_settings_->firewall()); + + http_request->SignalWorkDone.connect(this, + &SingleLoginAttempt::OnHttpTestDone); + http_request->Start(); + http_request->Release(); +} + +void SingleLoginAttempt::OnHttpTestDone(talk_base::SignalThread* thread) { + DCHECK(thread); + + talk_base::AsyncHttpRequest* request = + static_cast<talk_base::AsyncHttpRequest*>(thread); + + if (request->response().scode == 200) { + // We were able to do an HTTP GET of www.google.com:80 + + // + // The original error should be reported + // + LoginFailure failure(LoginFailure::XMPP_ERROR, code_, subcode_); + SignalLoginFailure(failure); + return; + } + + // Otherwise lets transmute the error into ERROR_SOCKET, and put the subcode + // as an indicator of what we think the problem might be. + +#if 0 + // TODO(sync): determine if notifier has an analogous situation. + + // + // We weren't able to do an HTTP GET of www.google.com:80 + // + GAutoupdater::Version version_logged_in(g_options.version_logged_in()); + GAutoupdater::Version version_installed(GetProductVersion().c_str()); + if (version_logged_in < version_installed) { + // + // Google Talk has been updated and can no longer connect to the Google + // Talk Service. Your firewall is probably not allowing the new version of + // Google Talk to connect to the internet. Please adjust your firewall + // settings to allow the new version of Google Talk to connect to the + // internet. + // + // We'll use the "error=1" to help figure this out for now. + // + LoginFailure failure(LoginFailure::XMPP_ERROR, + buzz::XmppEngine::ERROR_SOCKET, + 1); + SignalLoginFailure(failure); + return; + } +#endif + + // + // Any other checking we can add here? + // + + // + // Google Talk is unable to use your internet connection. Either your network + // isn't configured or Google Talk is being blocked by a local firewall. + // + // We'll use the "error=0" to help figure this out for now + // + LoginFailure failure(LoginFailure::XMPP_ERROR, + buzz::XmppEngine::ERROR_SOCKET, + 0); + SignalLoginFailure(failure); +} + +void SingleLoginAttempt::OnClientStateChange(buzz::XmppEngine::State state) { + if (state_ == state) + return; + + buzz::XmppEngine::State previous_state = state_; + state_ = state; + + switch (state) { + case buzz::XmppEngine::STATE_NONE: + case buzz::XmppEngine::STATE_START: + case buzz::XmppEngine::STATE_OPENING: + // Do nothing. + break; + case buzz::XmppEngine::STATE_OPEN: + successful_connection_ = true; + break; + case buzz::XmppEngine::STATE_CLOSED: + OnClientStateChangeClosed(previous_state); + break; + } + SignalClientStateChange(state); + if (state_ == buzz::XmppEngine::STATE_CLOSED) { + OnClientStateChange(buzz::XmppEngine::STATE_NONE); + } +} + +void SingleLoginAttempt::ClearClient() { + if (client_ != NULL) { + client_->Disconnect(); + + // If this assertion goes off, it means that the disconnect didn't occur + // properly. See SingleLoginAttempt::OnClientStateChange, + // case XmppEngine::STATE_CLOSED + DCHECK(!client_); + } +} + +void SingleLoginAttempt::OnClientStateChangeClosed( + buzz::XmppEngine::State previous_state) { + buzz::XmppEngine::Error error = buzz::XmppEngine::ERROR_NONE; + int error_subcode = 0; + buzz::XmlElement* stream_error_ptr; + GetClientErrorInformation(client_, + &error, + &error_subcode, + &stream_error_ptr); + scoped_ptr<buzz::XmlElement> stream_error(stream_error_ptr); + + client_->SignalStateChange.disconnect(this); + client_ = NULL; + + if (error == buzz::XmppEngine::ERROR_NONE) { + SignalLogoff(); + return; + } else if (previous_state == buzz::XmppEngine::STATE_OPEN) { + // Handler should attempt reconnect. + SignalUnexpectedDisconnect(); + return; + } else { + HandleConnectionError(error, error_subcode, stream_error.get()); + } +} + +void SingleLoginAttempt::HandleConnectionPasswordError() { + LOG(INFO) << "SingleLoginAttempt::HandleConnectionPasswordError"; + LoginFailure failure(LoginFailure::XMPP_ERROR, code_, subcode_); + SignalLoginFailure(failure); +} + +void SingleLoginAttempt::HandleConnectionError( + buzz::XmppEngine::Error code, + int subcode, + const buzz::XmlElement* stream_error) { + LOG(INFO) << "(" << code << ", " << subcode << ")"; + + // Save off the error code information, so we can use it to tell the user + // what went wrong if all else fails. + code_ = code; + subcode_ = subcode; + if ((code_ == buzz::XmppEngine::ERROR_UNAUTHORIZED) || + (code_ == buzz::XmppEngine::ERROR_MISSING_USERNAME)) { + // There was a problem with credentials (username/password). + HandleConnectionPasswordError(); + return; + } + + // Unexpected disconnect, + // Unreachable host, + // Or internal server binding error - + // All these are temporary problems, so continue reconnecting. + + // GaiaAuth signals this directly via SignalCertificateExpired, but + // SChannelAdapter propagates the error through SocketWindow as a socket + // error. + if (code_ == buzz::XmppEngine::ERROR_SOCKET && + subcode_ == SEC_E_CERT_EXPIRED) { + certificate_expired_ = true; + } + + login_settings_->modifiable_user_settings()->set_resource(""); + + // Look for stream::error server redirection stanza "see-other-host". + if (stream_error) { + const buzz::XmlElement* other = + stream_error->FirstNamed(buzz::QN_XSTREAM_SEE_OTHER_HOST); + if (other) { + const buzz::XmlElement* text = + stream_error->FirstNamed(buzz::QN_XSTREAM_TEXT); + if (text) { + // Yep, its a "stream:error" with "see-other-host" text, let's parse + // out the server:port, and then reconnect with that. + const std::string& redirect = text->BodyText(); + size_t colon = redirect.find(":"); + int redirect_port = kDefaultXmppPort; + std::string redirect_server; + if (colon == std::string::npos) { + redirect_server = redirect; + } else { + redirect_server = redirect.substr(0, colon); + const std::string& port_text = redirect.substr(colon + 1); + std::istringstream ist(port_text); + ist >> redirect_port; + } + // We never allow a redirect to port 0. + if (redirect_port == 0) { + redirect_port = kDefaultXmppPort; + } + SignalRedirect(redirect_server, redirect_port); + // May be deleted at this point. + return; + } + } + } + + DCHECK(connection_generator_.get()); + if (!connection_generator_.get()) { + return; + } + + // Iterate to the next possible connection (still trying to connect). + UseNextConnection(); +} + +} // namespace notifier diff --git a/jingle/notifier/communicator/single_login_attempt.h b/jingle/notifier/communicator/single_login_attempt.h new file mode 100644 index 0000000..3595c0fb --- /dev/null +++ b/jingle/notifier/communicator/single_login_attempt.h @@ -0,0 +1,138 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef JINGLE_NOTIFIER_COMMUNICATOR_SINGLE_LOGIN_ATTEMPT_H_ +#define JINGLE_NOTIFIER_COMMUNICATOR_SINGLE_LOGIN_ATTEMPT_H_ + +#include <string> + +#include "jingle/notifier/communicator/login.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/sigslot.h" +#include "talk/base/task.h" +#include "talk/xmpp/xmppengine.h" + +namespace buzz { +class AsyncSocket; +class PreXmppAuth; +class XmppClient; +class XmppClientSettings; +class XmppClientSettings; +} + +namespace talk_base { +class FirewallManager; +struct ProxyInfo; +class SignalThread; +class Task; +} + +namespace notifier { + +class ConnectionSettings; +class LoginFailure; +class LoginSettings; +struct ServerInformation; +class XmppConnectionGenerator; + +// Handles all of the aspects of a single login attempt (across multiple ip +// addresses) and maintainence. By containing this within one class, when +// another login attempt is made, this class will be disposed and all of the +// signalling for the previous login attempt will be cleaned up immediately. +// +// This is a task to allow for cleaning this up when a signal is being fired. +// Technically, delete this during the firing of a signal could work but it is +// fragile. +class SingleLoginAttempt : public talk_base::Task, public sigslot::has_slots<> { + public: + SingleLoginAttempt(talk_base::TaskParent* parent, + LoginSettings* login_settings, + bool successful_connection); + ~SingleLoginAttempt(); + virtual int ProcessStart(); + void UseNextConnection(); + void UseCurrentConnection(); + + buzz::XmppClient* xmpp_client() { + return client_; + } + + // Returns the proxy that is being used to connect (or the default proxy + // information if all attempted connections failed). + const talk_base::ProxyInfo& proxy() const; + + // Typically handled by creating a new SingleLoginAttempt and doing + // StartConnection. + sigslot::signal0<> SignalUnexpectedDisconnect; + + // Typically handled by storing the redirect for 5 seconds, and setting it + // into LoginSettings, then creating a new SingleLoginAttempt, and doing + // StartConnection. + // + // SignalRedirect(const std::string& redirect_server, int redirect_port); + sigslot::signal2<const std::string&, int> SignalRedirect; + + sigslot::signal0<> SignalNeedAutoReconnect; + + // SignalClientStateChange(buzz::XmppEngine::State new_state); + sigslot::signal1<buzz::XmppEngine::State> SignalClientStateChange; + + // See the LoginFailure for how to handle this. + sigslot::signal1<const LoginFailure&> SignalLoginFailure; + + // Sent when there is a graceful log-off (state goes to closed with no + // error). + sigslot::signal0<> SignalLogoff; + + sigslot::repeater2<const char*, int> SignalLogInput; + sigslot::repeater2<const char*, int> SignalLogOutput; + + protected: + virtual void Stop(); + + private: + void DoLogin(const ConnectionSettings& connection_settings); + buzz::AsyncSocket* CreateSocket(const buzz::XmppClientSettings& xcs); + static buzz::PreXmppAuth* CreatePreXmppAuth( + const buzz::XmppClientSettings& xcs); + + // Cleans up any xmpp client state to get ready for a new one. + void ClearClient(); + + void HandleConnectionError( + buzz::XmppEngine::Error code, + int subcode, + const buzz::XmlElement* stream_error); + void HandleConnectionPasswordError(); + + void DiagnoseConnectionError(); + void OnHttpTestDone(talk_base::SignalThread* thread); + + void OnAuthenticationError(); + void OnCertificateExpired(); + void OnFreshAuthCookie(const std::string& auth_cookie); + void OnClientStateChange(buzz::XmppEngine::State state); + void OnClientStateChangeClosed(buzz::XmppEngine::State previous_state); + void OnAttemptedAllConnections(bool successfully_resolved_dns, + int first_dns_error); + + bool auto_reconnect() const; + + buzz::XmppEngine::State state_; + buzz::XmppEngine::Error code_; + int subcode_; + bool need_authentication_; + bool certificate_expired_; + bool cookie_refreshed_; + bool successful_connection_; + LoginSettings* login_settings_; + buzz::XmppClient* client_; + scoped_ptr<XmppConnectionGenerator> connection_generator_; + + DISALLOW_COPY_AND_ASSIGN(SingleLoginAttempt); +}; + +} // namespace notifier + +#endif // JINGLE_NOTIFIER_COMMUNICATOR_SINGLE_LOGIN_ATTEMPT_H_ diff --git a/jingle/notifier/communicator/ssl_socket_adapter.cc b/jingle/notifier/communicator/ssl_socket_adapter.cc new file mode 100644 index 0000000..a753710 --- /dev/null +++ b/jingle/notifier/communicator/ssl_socket_adapter.cc @@ -0,0 +1,389 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "jingle/notifier/communicator/ssl_socket_adapter.h" + +#include "base/compiler_specific.h" +#include "base/message_loop.h" +#include "net/base/address_list.h" +#include "net/base/net_errors.h" +#include "net/base/ssl_config_service.h" +#include "net/base/sys_addrinfo.h" +#include "net/socket/client_socket_factory.h" +#include "net/url_request/url_request_context.h" + +namespace notifier { + +namespace { + +// Convert values from <errno.h> to values from "net/base/net_errors.h" +int MapPosixError(int err) { + // There are numerous posix error codes, but these are the ones we thus far + // find interesting. + switch (err) { + case EAGAIN: +#if EWOULDBLOCK != EAGAIN + case EWOULDBLOCK: +#endif + return net::ERR_IO_PENDING; + case ENETDOWN: + return net::ERR_INTERNET_DISCONNECTED; + case ETIMEDOUT: + return net::ERR_TIMED_OUT; + case ECONNRESET: + case ENETRESET: // Related to keep-alive + return net::ERR_CONNECTION_RESET; + case ECONNABORTED: + return net::ERR_CONNECTION_ABORTED; + case ECONNREFUSED: + return net::ERR_CONNECTION_REFUSED; + case EHOSTUNREACH: + case ENETUNREACH: + return net::ERR_ADDRESS_UNREACHABLE; + case EADDRNOTAVAIL: + return net::ERR_ADDRESS_INVALID; + case 0: + return net::OK; + default: + LOG(WARNING) << "Unknown error " << err << " mapped to net::ERR_FAILED"; + return net::ERR_FAILED; + } +} + +} // namespace + +SSLSocketAdapter* SSLSocketAdapter::Create(AsyncSocket* socket) { + return new SSLSocketAdapter(socket); +} + +SSLSocketAdapter::SSLSocketAdapter(AsyncSocket* socket) + : SSLAdapter(socket), + ignore_bad_cert_(false), + ALLOW_THIS_IN_INITIALIZER_LIST( + connected_callback_(this, &SSLSocketAdapter::OnConnected)), + ALLOW_THIS_IN_INITIALIZER_LIST( + io_callback_(this, &SSLSocketAdapter::OnIO)), + ssl_connected_(false), + state_(STATE_NONE) { + transport_socket_ = new TransportSocket(socket, this); +} + +int SSLSocketAdapter::StartSSL(const char* hostname, bool restartable) { + DCHECK(!restartable); + hostname_ = hostname; + + if (socket_->GetState() != Socket::CS_CONNECTED) { + state_ = STATE_SSL_WAIT; + return 0; + } else { + return BeginSSL(); + } +} + +int SSLSocketAdapter::BeginSSL() { + if (!MessageLoop::current()) { + // Certificate verification is done via the Chrome message loop. + // Without this check, if we don't have a chrome message loop the + // SSL connection just hangs silently. + LOG(DFATAL) << "Chrome message loop (needed by SSL certificate " + << "verification) does not exist"; + return net::ERR_UNEXPECTED; + } + + // SSLConfigService is not thread-safe, and the default values for SSLConfig + // are correct for us, so we don't use the config service to initialize this + // object. + net::SSLConfig ssl_config; + transport_socket_->set_addr(talk_base::SocketAddress(hostname_, 0)); + ssl_socket_.reset( + net::ClientSocketFactory::GetDefaultFactory()->CreateSSLClientSocket( + transport_socket_, hostname_.c_str(), ssl_config)); + + int result = ssl_socket_->Connect(&connected_callback_); + + if (result == net::ERR_IO_PENDING || result == net::OK) { + return 0; + } else { + LOG(ERROR) << "Could not start SSL: " << net::ErrorToString(result); + return result; + } +} + +int SSLSocketAdapter::Send(const void* buf, size_t len) { + if (!ssl_connected_) { + return AsyncSocketAdapter::Send(buf, len); + } else { + scoped_refptr<net::IOBuffer> transport_buf = new net::IOBuffer(len); + memcpy(transport_buf->data(), buf, len); + + int result = ssl_socket_->Write(transport_buf, len, NULL); + if (result == net::ERR_IO_PENDING) { + SetError(EWOULDBLOCK); + } + transport_buf = NULL; + return result; + } +} + +int SSLSocketAdapter::Recv(void* buf, size_t len) { + if (!ssl_connected_) { + return AsyncSocketAdapter::Recv(buf, len); + } + + switch (state_) { + case STATE_NONE: { + transport_buf_ = new net::IOBuffer(len); + int result = ssl_socket_->Read(transport_buf_, len, &io_callback_); + if (result >= 0) { + memcpy(buf, transport_buf_->data(), len); + } + + if (result == net::ERR_IO_PENDING) { + state_ = STATE_READ; + SetError(EWOULDBLOCK); + } else { + if (result < 0) { + SetError(result); + LOG(INFO) << "Socket error " << result; + } + transport_buf_ = NULL; + } + return result; + } + case STATE_READ_COMPLETE: + memcpy(buf, transport_buf_->data(), len); + transport_buf_ = NULL; + state_ = STATE_NONE; + return data_transferred_; + + case STATE_READ: + case STATE_WRITE: + case STATE_WRITE_COMPLETE: + case STATE_SSL_WAIT: + SetError(EWOULDBLOCK); + return -1; + + default: + NOTREACHED(); + break; + } + return -1; +} + +void SSLSocketAdapter::OnConnected(int result) { + if (result == net::OK) { + ssl_connected_ = true; + OnConnectEvent(this); + } else { + LOG(WARNING) << "OnConnected failed with error " << result; + } +} + +void SSLSocketAdapter::OnIO(int result) { + switch (state_) { + case STATE_READ: + state_ = STATE_READ_COMPLETE; + data_transferred_ = result; + AsyncSocketAdapter::OnReadEvent(this); + break; + case STATE_WRITE: + state_ = STATE_WRITE_COMPLETE; + data_transferred_ = result; + AsyncSocketAdapter::OnWriteEvent(this); + break; + case STATE_NONE: + case STATE_READ_COMPLETE: + case STATE_WRITE_COMPLETE: + case STATE_SSL_WAIT: + default: + NOTREACHED(); + break; + } +} + +void SSLSocketAdapter::OnReadEvent(talk_base::AsyncSocket* socket) { + if (!transport_socket_->OnReadEvent(socket)) + AsyncSocketAdapter::OnReadEvent(socket); +} + +void SSLSocketAdapter::OnWriteEvent(talk_base::AsyncSocket* socket) { + if (!transport_socket_->OnWriteEvent(socket)) + AsyncSocketAdapter::OnWriteEvent(socket); +} + +void SSLSocketAdapter::OnConnectEvent(talk_base::AsyncSocket* socket) { + if (state_ != STATE_SSL_WAIT) { + AsyncSocketAdapter::OnConnectEvent(socket); + } else { + state_ = STATE_NONE; + int result = BeginSSL(); + if (0 != result) { + // TODO(zork): Handle this case gracefully. + LOG(WARNING) << "BeginSSL() failed with " << result; + } + } +} + +TransportSocket::TransportSocket(talk_base::AsyncSocket* socket, + SSLSocketAdapter *ssl_adapter) + : connect_callback_(NULL), + read_callback_(NULL), + write_callback_(NULL), + read_buffer_len_(0), + write_buffer_len_(0), + socket_(socket) { + socket_->SignalConnectEvent.connect(this, &TransportSocket::OnConnectEvent); +} + +int TransportSocket::Connect(net::CompletionCallback* callback) { + connect_callback_ = callback; + return socket_->Connect(addr_); +} + +void TransportSocket::Disconnect() { + socket_->Close(); +} + +bool TransportSocket::IsConnected() const { + return (socket_->GetState() == talk_base::Socket::CS_CONNECTED); +} + +bool TransportSocket::IsConnectedAndIdle() const { + // Not implemented. + NOTREACHED(); + return false; +} + +int TransportSocket::GetPeerAddress(net::AddressList* address) const { + talk_base::SocketAddress socket_address = socket_->GetRemoteAddress(); + + // libjingle supports only IPv4 addresses. + sockaddr_in ipv4addr; + socket_address.ToSockAddr(&ipv4addr); + + struct addrinfo ai; + memset(&ai, 0, sizeof(ai)); + ai.ai_family = ipv4addr.sin_family; + ai.ai_socktype = SOCK_STREAM; + ai.ai_protocol = IPPROTO_TCP; + ai.ai_addr = reinterpret_cast<struct sockaddr*>(&ipv4addr); + ai.ai_addrlen = sizeof(ipv4addr); + + address->Copy(&ai, false); + return net::OK; +} + +int TransportSocket::Read(net::IOBuffer* buf, int buf_len, + net::CompletionCallback* callback) { + DCHECK(buf); + DCHECK(!read_callback_); + DCHECK(!read_buffer_.get()); + int result = socket_->Recv(buf->data(), buf_len); + if (result < 0) { + result = MapPosixError(socket_->GetError()); + if (result == net::ERR_IO_PENDING) { + read_callback_ = callback; + read_buffer_ = buf; + read_buffer_len_ = buf_len; + } + } + return result; +} + +int TransportSocket::Write(net::IOBuffer* buf, int buf_len, + net::CompletionCallback* callback) { + DCHECK(buf); + DCHECK(!write_callback_); + DCHECK(!write_buffer_.get()); + int result = socket_->Send(buf->data(), buf_len); + if (result < 0) { + result = MapPosixError(socket_->GetError()); + if (result == net::ERR_IO_PENDING) { + write_callback_ = callback; + write_buffer_ = buf; + write_buffer_len_ = buf_len; + } + } + return result; +} + +bool TransportSocket::SetReceiveBufferSize(int32 size) { + // Not implemented. + return false; +} + +bool TransportSocket::SetSendBufferSize(int32 size) { + // Not implemented. + return false; +} + +void TransportSocket::OnConnectEvent(talk_base::AsyncSocket * socket) { + if (connect_callback_) { + net::CompletionCallback *callback = connect_callback_; + connect_callback_ = NULL; + callback->RunWithParams(Tuple1<int>(MapPosixError(socket_->GetError()))); + } else { + LOG(WARNING) << "OnConnectEvent called with no callback."; + } +} + +bool TransportSocket::OnReadEvent(talk_base::AsyncSocket* socket) { + if (read_callback_) { + DCHECK(read_buffer_.get()); + net::CompletionCallback* callback = read_callback_; + scoped_refptr<net::IOBuffer> buffer = read_buffer_; + int buffer_len = read_buffer_len_; + + read_callback_ = NULL; + read_buffer_ = NULL; + read_buffer_len_ = 0; + + int result = socket_->Recv(buffer->data(), buffer_len); + if (result < 0) { + result = MapPosixError(socket_->GetError()); + if (result == net::ERR_IO_PENDING) { + read_callback_ = callback; + read_buffer_ = buffer; + read_buffer_len_ = buffer_len; + return true; + } + } + callback->RunWithParams(Tuple1<int>(result)); + return true; + } else { + LOG(WARNING) << "OnReadEvent called with no callback."; + return false; + } +} + +bool TransportSocket::OnWriteEvent(talk_base::AsyncSocket* socket) { + if (write_callback_) { + DCHECK(write_buffer_.get()); + net::CompletionCallback* callback = write_callback_; + scoped_refptr<net::IOBuffer> buffer = write_buffer_; + int buffer_len = write_buffer_len_; + + write_callback_ = NULL; + write_buffer_ = NULL; + write_buffer_len_ = 0; + + int result = socket_->Send(buffer->data(), buffer_len); + if (result < 0) { + result = MapPosixError(socket_->GetError()); + if (result == net::ERR_IO_PENDING) { + write_callback_ = callback; + write_buffer_ = buffer; + write_buffer_len_ = buffer_len; + return true; + } + } + callback->RunWithParams(Tuple1<int>(result)); + return true; + } else { + LOG(WARNING) << "OnWriteEvent called with no callback."; + return false; + } +} + +} // namespace notifier diff --git a/jingle/notifier/communicator/ssl_socket_adapter.h b/jingle/notifier/communicator/ssl_socket_adapter.h new file mode 100644 index 0000000..a97cfaa --- /dev/null +++ b/jingle/notifier/communicator/ssl_socket_adapter.h @@ -0,0 +1,134 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef JINGLE_NOTIFIER_COMMUNICATOR_SSL_SOCKET_ADAPTER_H_ +#define JINGLE_NOTIFIER_COMMUNICATOR_SSL_SOCKET_ADAPTER_H_ + +#include "base/scoped_ptr.h" +#include "net/base/completion_callback.h" +#include "net/base/io_buffer.h" +#include "net/base/net_errors.h" +#include "net/base/net_log.h" +#include "net/socket/client_socket.h" +#include "net/socket/ssl_client_socket.h" +#include "talk/base/asyncsocket.h" +#include "talk/base/ssladapter.h" + +namespace notifier { + +class SSLSocketAdapter; + +// This class provides a wrapper to libjingle's talk_base::AsyncSocket that +// implements Chromium's net::ClientSocket interface. It's used by +// SSLSocketAdapter to enable Chromium's SSL implementation to work over +// libjingle's socket class. +class TransportSocket : public net::ClientSocket, public sigslot::has_slots<> { + public: + TransportSocket(talk_base::AsyncSocket* socket, + SSLSocketAdapter *ssl_adapter); + + void set_addr(const talk_base::SocketAddress& addr) { + addr_ = addr; + } + + // net::ClientSocket implementation + + virtual int Connect(net::CompletionCallback* callback); + virtual void Disconnect(); + virtual bool IsConnected() const; + virtual bool IsConnectedAndIdle() const; + virtual int GetPeerAddress(net::AddressList* address) const; + virtual const net::BoundNetLog& NetLog() const { return net_log_; } + + // net::Socket implementation + + virtual int Read(net::IOBuffer* buf, int buf_len, + net::CompletionCallback* callback); + virtual int Write(net::IOBuffer* buf, int buf_len, + net::CompletionCallback* callback); + virtual bool SetReceiveBufferSize(int32 size); + virtual bool SetSendBufferSize(int32 size); + + private: + friend class SSLSocketAdapter; + + void OnConnectEvent(talk_base::AsyncSocket * socket); + bool OnReadEvent(talk_base::AsyncSocket * socket); + bool OnWriteEvent(talk_base::AsyncSocket * socket); + + net::CompletionCallback* connect_callback_; + net::CompletionCallback* read_callback_; + net::CompletionCallback* write_callback_; + + scoped_refptr<net::IOBuffer> read_buffer_; + int read_buffer_len_; + scoped_refptr<net::IOBuffer> write_buffer_; + int write_buffer_len_; + + net::BoundNetLog net_log_; + + talk_base::AsyncSocket *socket_; + talk_base::SocketAddress addr_; + + DISALLOW_COPY_AND_ASSIGN(TransportSocket); +}; + +// This provides a talk_base::AsyncSocketAdapter interface around Chromium's +// net::SSLClientSocket class. This allows notifier to use Chromium's SSL +// implementation instead of OpenSSL. +class SSLSocketAdapter : public talk_base::SSLAdapter { + public: + explicit SSLSocketAdapter(talk_base::AsyncSocket* socket); + + // StartSSL returns 0 if successful, or non-zero on failure. + // If StartSSL is called while the socket is closed or connecting, the SSL + // negotiation will begin as soon as the socket connects. + // + // restartable is not implemented, and must be set to false. + virtual int StartSSL(const char* hostname, bool restartable); + + // Create the default SSL adapter for this platform. + static SSLSocketAdapter* Create(AsyncSocket* socket); + + virtual int Send(const void* pv, size_t cb); + virtual int Recv(void* pv, size_t cb); + + private: + friend class TransportSocket; + + enum State { + STATE_NONE, + STATE_READ, + STATE_READ_COMPLETE, + STATE_WRITE, + STATE_WRITE_COMPLETE, + STATE_SSL_WAIT + }; + + void OnConnected(int result); + void OnIO(int result); + + void OnReadEvent(talk_base::AsyncSocket * socket); + void OnWriteEvent(talk_base::AsyncSocket * socket); + void OnConnectEvent(talk_base::AsyncSocket * socket); + + int BeginSSL(); + + bool ignore_bad_cert_; + std::string hostname_; + TransportSocket* transport_socket_; + scoped_ptr<net::SSLClientSocket> ssl_socket_; + net::CompletionCallbackImpl<SSLSocketAdapter> connected_callback_; + net::CompletionCallbackImpl<SSLSocketAdapter> io_callback_; + bool ssl_connected_; + State state_; + scoped_refptr<net::IOBuffer> transport_buf_; + int data_transferred_; + + DISALLOW_COPY_AND_ASSIGN(SSLSocketAdapter); +}; + +} // namespace notifier + +#endif // JINGLE_NOTIFIER_COMMUNICATOR_SSL_SOCKET_ADAPTER_H_ diff --git a/jingle/notifier/communicator/xmpp_connection_generator.cc b/jingle/notifier/communicator/xmpp_connection_generator.cc new file mode 100644 index 0000000..abadd35 --- /dev/null +++ b/jingle/notifier/communicator/xmpp_connection_generator.cc @@ -0,0 +1,214 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// XmppConnectionGenerator does the following algorithm: +// proxy = ResolveProxyInformation(connection_options) +// for server in server_list +// get dns_addresses for server +// connection_list = (dns_addresses X connection methods X proxy).shuffle() +// for connection in connection_list +// yield connection + +#include "jingle/notifier/communicator/xmpp_connection_generator.h" + +#if defined(OS_WIN) +#include <winsock2.h> +#elif defined(OS_POSIX) +#include <arpa/inet.h> +#endif + +#include <vector> + +#include "base/callback.h" +#include "base/compiler_specific.h" +#include "base/logging.h" +#include "jingle/notifier/base/signal_thread_task.h" +#include "jingle/notifier/communicator/connection_options.h" +#include "jingle/notifier/communicator/connection_settings.h" +#include "jingle/notifier/communicator/product_info.h" +#include "net/base/net_errors.h" +#include "net/base/sys_addrinfo.h" +#include "talk/base/httpcommon-inl.h" +#include "talk/base/task.h" +#include "talk/base/thread.h" +#include "talk/xmpp/prexmppauth.h" +#include "talk/xmpp/xmppclientsettings.h" +#include "talk/xmpp/xmppengine.h" + +namespace notifier { + +XmppConnectionGenerator::XmppConnectionGenerator( + talk_base::Task* parent, + const scoped_refptr<net::HostResolver>& host_resolver, + const ConnectionOptions* options, + bool proxy_only, + const ServerInformation* server_list, + int server_count) + : host_resolver_(host_resolver), + resolve_callback_( + ALLOW_THIS_IN_INITIALIZER_LIST( + NewCallback(this, + &XmppConnectionGenerator::OnServerDNSResolved))), + settings_list_(new ConnectionSettingsList()), + settings_index_(0), + server_list_(new ServerInformation[server_count]), + server_count_(server_count), + server_index_(-1), + proxy_only_(proxy_only), + successfully_resolved_dns_(false), + first_dns_error_(0), + options_(options), + parent_(parent) { + DCHECK(host_resolver); + DCHECK(parent); + DCHECK(options); + DCHECK_GT(server_count_, 0); + for (int i = 0; i < server_count_; ++i) { + server_list_[i] = server_list[i]; + } +} + +XmppConnectionGenerator::~XmppConnectionGenerator() { + LOG(INFO) << "XmppConnectionGenerator::~XmppConnectionGenerator"; +} + +const talk_base::ProxyInfo& XmppConnectionGenerator::proxy() const { + DCHECK(settings_list_.get()); + if (settings_index_ >= settings_list_->GetCount()) { + return settings_list_->proxy(); + } + + ConnectionSettings* settings = settings_list_->GetSettings(settings_index_); + return settings->proxy(); +} + +// Starts resolving proxy information. +void XmppConnectionGenerator::StartGenerating() { + LOG(INFO) << "XmppConnectionGenerator::StartGenerating"; + + // TODO(akalin): Detect proxy settings once we use Chrome sockets. + + // Start iterating through the connections (which are generated on demand). + UseNextConnection(); +} + +void XmppConnectionGenerator::UseNextConnection() { + DCHECK(settings_list_.get()); + // Loop until we can use a connection or we run out of connections + // to try. + while (true) { + // Iterate to the next possible connection. + settings_index_++; + if (settings_index_ < settings_list_->GetCount()) { + // We have more connection settings in the settings_list_ to + // try, kick off the next one. + UseCurrentConnection(); + return; + } + + // Iterate to the next possible server. + server_index_++; + if (server_index_ >= server_count_) { + // All out of possibilities. + HandleExhaustedConnections(); + return; + } + + // Resolve the server. + const net::HostPortPair& server = + server_list_[server_index_].server; + net::HostResolver::RequestInfo request_info( + server.host, server.port); + int status = + host_resolver_.Resolve( + request_info, &address_list_, resolve_callback_.get(), + bound_net_log_); + if (status == net::ERR_IO_PENDING) { + // resolve_callback_ will call us when it's called. + return; + } + HandleServerDNSResolved(status); + } +} + +void XmppConnectionGenerator::OnServerDNSResolved(int status) { + DCHECK_NE(status, net::ERR_IO_PENDING); + HandleServerDNSResolved(status); + // Reenter loop. + UseNextConnection(); +} + +void XmppConnectionGenerator::HandleServerDNSResolved(int status) { + DCHECK_NE(status, net::ERR_IO_PENDING); + LOG(INFO) << "XmppConnectionGenerator::HandleServerDNSResolved"; + // Print logging info. + LOG(INFO) << " server: " + << server_list_[server_index_].server.ToString() + << ", error: " << status; + if (status != net::OK) { + if (first_dns_error_ == 0) { + first_dns_error_ = status; + } + return; + } + + // Slurp the addresses into a vector. + std::vector<uint32> ip_list; + for (const addrinfo* addr = address_list_.head(); + addr != NULL; addr = addr->ai_next) { + const sockaddr_in& sockaddr = + *reinterpret_cast<const sockaddr_in*>(addr->ai_addr); + uint32 ip = ntohl(sockaddr.sin_addr.s_addr); + ip_list.push_back(ip); + } + successfully_resolved_dns_ = !ip_list.empty(); + + for (int i = 0; i < static_cast<int>(ip_list.size()); ++i) { + LOG(INFO) + << " ip " << i << " : " + << talk_base::SocketAddress::IPToString(ip_list[i]); + } + + // Build the ip list. + DCHECK(settings_list_.get()); + settings_index_ = -1; + settings_list_->ClearPermutations(); + settings_list_->AddPermutations( + server_list_[server_index_].server.host, + ip_list, + server_list_[server_index_].server.port, + server_list_[server_index_].special_port_magic, + proxy_only_); +} + +static const char* const PROTO_NAMES[cricket::PROTO_LAST + 1] = { + "udp", "tcp", "ssltcp" +}; + +static const char* ProtocolToString(cricket::ProtocolType proto) { + return PROTO_NAMES[proto]; +} + +void XmppConnectionGenerator::UseCurrentConnection() { + LOG(INFO) << "XmppConnectionGenerator::UseCurrentConnection"; + + ConnectionSettings* settings = settings_list_->GetSettings(settings_index_); + LOG(INFO) << "*** Attempting " + << ProtocolToString(settings->protocol()) << " connection to " + << settings->server().IPAsString() << ":" + << settings->server().port() + << " (via " << ProxyToString(settings->proxy().type) + << " proxy @ " << settings->proxy().address.IPAsString() << ":" + << settings->proxy().address.port() << ")"; + + SignalNewSettings(*settings); +} + +void XmppConnectionGenerator::HandleExhaustedConnections() { + LOG(INFO) << "(" << buzz::XmppEngine::ERROR_SOCKET + << ", " << first_dns_error_ << ")"; + SignalExhaustedSettings(successfully_resolved_dns_, first_dns_error_); +} + +} // namespace notifier diff --git a/jingle/notifier/communicator/xmpp_connection_generator.h b/jingle/notifier/communicator/xmpp_connection_generator.h new file mode 100644 index 0000000..43b5329 --- /dev/null +++ b/jingle/notifier/communicator/xmpp_connection_generator.h @@ -0,0 +1,94 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef JINGLE_NOTIFIER_COMMUNICATOR_XMPP_CONNECTION_GENERATOR_H_ +#define JINGLE_NOTIFIER_COMMUNICATOR_XMPP_CONNECTION_GENERATOR_H_ + +#include <vector> + +#include "base/ref_counted.h" +#include "net/base/address_list.h" +#include "net/base/completion_callback.h" +#include "net/base/host_port_pair.h" +#include "net/base/host_resolver.h" +#include "net/base/net_log.h" +#include "talk/base/scoped_ptr.h" +#include "talk/base/sigslot.h" + +namespace talk_base { +struct ProxyInfo; +class SignalThread; +class Task; +} + +namespace notifier { + +class ConnectionOptions; +class ConnectionSettings; +class ConnectionSettingsList; + +struct ServerInformation { + net::HostPortPair server; + bool special_port_magic; +}; + +// Resolves dns names and iterates through the various ip address and transport +// combinations. +class XmppConnectionGenerator : public sigslot::has_slots<> { + public: + // parent is the parent for any tasks needed during this operation. + // proxy_only indicates if true connections are only attempted using the + // proxy. + // server_list is the list of connections to attempt in priority order. + // server_count is the number of items in the server list. + XmppConnectionGenerator( + talk_base::Task* parent, + const scoped_refptr<net::HostResolver>& host_resolver, + const ConnectionOptions* options, + bool proxy_only, + const ServerInformation* server_list, + int server_count); + ~XmppConnectionGenerator(); + + // Only call this once. Create a new XmppConnectionGenerator and delete the + // current one if the process needs to start again. + void StartGenerating(); + + void UseNextConnection(); + void UseCurrentConnection(); + + const talk_base::ProxyInfo& proxy() const; + + sigslot::signal1<const ConnectionSettings&> SignalNewSettings; + + // SignalExhaustedSettings(bool successfully_resolved_dns, + // int first_dns_error); + sigslot::signal2<bool, int> SignalExhaustedSettings; + + private: + void OnServerDNSResolved(int status); + void HandleServerDNSResolved(int status); + void HandleExhaustedConnections(); + + net::SingleRequestHostResolver host_resolver_; + scoped_ptr<net::CompletionCallback> resolve_callback_; + net::AddressList address_list_; + net::BoundNetLog bound_net_log_; + talk_base::scoped_ptr<ConnectionSettingsList> settings_list_; + int settings_index_; // The setting that is currently being used. + talk_base::scoped_array<ServerInformation> server_list_; + int server_count_; + int server_index_; // The server that is current being used. + bool proxy_only_; + bool successfully_resolved_dns_; + int first_dns_error_; + const ConnectionOptions* options_; + + talk_base::Task* parent_; + DISALLOW_COPY_AND_ASSIGN(XmppConnectionGenerator); +}; + +} // namespace notifier + +#endif // JINGLE_NOTIFIER_COMMUNICATOR_XMPP_CONNECTION_GENERATOR_H_ diff --git a/jingle/notifier/communicator/xmpp_socket_adapter.cc b/jingle/notifier/communicator/xmpp_socket_adapter.cc new file mode 100644 index 0000000..7d7a2e9 --- /dev/null +++ b/jingle/notifier/communicator/xmpp_socket_adapter.cc @@ -0,0 +1,429 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "jingle/notifier/communicator/xmpp_socket_adapter.h" + +#include <iomanip> +#include <string> + +#include "base/logging.h" +#include "jingle/notifier/base/ssl_adapter.h" +#include "jingle/notifier/communicator/product_info.h" +#include "talk/base/byteorder.h" +#include "talk/base/common.h" +#include "talk/base/firewallsocketserver.h" +#include "talk/base/logging.h" +#include "talk/base/socketadapters.h" +#include "talk/base/ssladapter.h" +#include "talk/base/thread.h" +#include "talk/xmpp/xmppengine.h" + +namespace notifier { + +XmppSocketAdapter::XmppSocketAdapter(const buzz::XmppClientSettings& xcs, + bool allow_unverified_certs) + : state_(STATE_CLOSED), + error_(ERROR_NONE), + wsa_error_(0), + socket_(NULL), + protocol_(xcs.protocol()), + firewall_(false), + write_buffer_(NULL), + write_buffer_length_(0), + write_buffer_capacity_(0), + allow_unverified_certs_(allow_unverified_certs) { + proxy_.type = xcs.proxy(); + proxy_.address.SetIP(xcs.proxy_host()); + proxy_.address.SetPort(xcs.proxy_port()); + proxy_.username = xcs.proxy_user(); + proxy_.password = xcs.proxy_pass(); +} + +XmppSocketAdapter::~XmppSocketAdapter() { + FreeState(); + + // Clean up any previous socket - cannot delete socket on close because close + // happens during the child socket's stack callback. + if (socket_) { + delete socket_; + socket_ = NULL; + } +} + +bool XmppSocketAdapter::FreeState() { + int code = 0; + + // Clean up the socket. + if (socket_ && !(state_ == STATE_CLOSED || state_ == STATE_CLOSING)) { + code = socket_->Close(); + } + + delete[] write_buffer_; + write_buffer_ = NULL; + write_buffer_length_ = 0; + write_buffer_capacity_ = 0; + + if (code) { + SetWSAError(code); + return false; + } + return true; +} + +bool XmppSocketAdapter::Connect(const talk_base::SocketAddress& addr) { + if (state_ != STATE_CLOSED) { + SetError(ERROR_WRONGSTATE); + return false; + } + + LOG(INFO) << "XmppSocketAdapter::Connect(" << addr.ToString() << ")"; + + // Clean up any previous socket - cannot delete socket on close because close + // happens during the child socket's stack callback. + if (socket_) { + delete socket_; + socket_ = NULL; + } + + talk_base::AsyncSocket* socket = + talk_base::Thread::Current()->socketserver()->CreateAsyncSocket( + SOCK_STREAM); + if (!socket) { + SetWSAError(WSA_NOT_ENOUGH_MEMORY); + return false; + } + + if (firewall_) { + // TODO(sync): Change this to make WSAAsyncSockets support current thread + // socket server. + talk_base::FirewallSocketServer* fw = + static_cast<talk_base::FirewallSocketServer*>( + talk_base::Thread::Current()->socketserver()); + socket = fw->WrapSocket(socket, SOCK_STREAM); + } + + if (proxy_.type) { + talk_base::AsyncSocket* proxy_socket = 0; + if (proxy_.type == talk_base::PROXY_SOCKS5) { + proxy_socket = new talk_base::AsyncSocksProxySocket( + socket, proxy_.address, proxy_.username, proxy_.password); + } else { + // Note: we are trying unknown proxies as HTTPS currently. + proxy_socket = new talk_base::AsyncHttpsProxySocket(socket, + GetUserAgentString(), proxy_.address, proxy_.username, + proxy_.password); + } + if (!proxy_socket) { + SetWSAError(WSA_NOT_ENOUGH_MEMORY); + delete socket; + return false; + } + socket = proxy_socket; // For our purposes the proxy is now the socket. + } + + if (protocol_ == cricket::PROTO_SSLTCP) { + talk_base::AsyncSocket *fake_ssl_socket = + new talk_base::AsyncSSLSocket(socket); + if (!fake_ssl_socket) { + SetWSAError(WSA_NOT_ENOUGH_MEMORY); + delete socket; + return false; + } + socket = fake_ssl_socket; // For our purposes the SSL socket is the socket. + } + +#if defined(FEATURE_ENABLE_SSL) + talk_base::SSLAdapter* ssl_adapter = notifier::CreateSSLAdapter(socket); + socket = ssl_adapter; // For our purposes the SSL adapter is the socket. +#endif + + socket->SignalReadEvent.connect(this, &XmppSocketAdapter::OnReadEvent); + socket->SignalWriteEvent.connect(this, &XmppSocketAdapter::OnWriteEvent); + socket->SignalConnectEvent.connect(this, &XmppSocketAdapter::OnConnectEvent); + socket->SignalCloseEvent.connect(this, &XmppSocketAdapter::OnCloseEvent); + + // The linux implementation of socket::Connect returns an error when the + // connect didn't complete yet. This can be distinguished from a failure + // because socket::IsBlocking is true. Perhaps, the linux implementation + // should be made to behave like the windows version which doesn't do this, + // but it seems to be a pattern with these methods that they return an error + // if the operation didn't complete in a sync fashion and one has to check + // IsBlocking to tell if was a "real" error. + if (socket->Connect(addr) == SOCKET_ERROR && !socket->IsBlocking()) { + SetWSAError(socket->GetError()); + delete socket; + return false; + } + + socket_ = socket; + state_ = STATE_CONNECTING; + return true; +} + +bool XmppSocketAdapter::Read(char* data, size_t len, size_t* len_read) { + if (len_read) + *len_read = 0; + + if (state_ <= STATE_CLOSING) { + SetError(ERROR_WRONGSTATE); + return false; + } + + DCHECK(socket_); + + if (IsOpen()) { + int result = socket_->Recv(data, len); + if (result < 0) { + if (!socket_->IsBlocking()) { + SetWSAError(socket_->GetError()); + return false; + } + + result = 0; + } + + if (len_read) + *len_read = result; + } + + return true; +} + +bool XmppSocketAdapter::Write(const char* data, size_t len) { + if (state_ <= STATE_CLOSING) { + // There may be data in a buffer that gets lost. Too bad! + SetError(ERROR_WRONGSTATE); + return false; + } + + DCHECK(socket_); + + size_t sent = 0; + + // Try an immediate write when there is no buffer and we aren't in SSL mode + // or opening the connection. + if (write_buffer_length_ == 0 && IsOpen()) { + int result = socket_->Send(data, len); + if (result < 0) { + if (!socket_->IsBlocking()) { + SetWSAError(socket_->GetError()); + return false; + } + result = 0; + } + + sent = static_cast<size_t>(result); + } + + // Buffer what we didn't send. + if (sent < len) { + QueueWriteData(data + sent, len - sent); + } + + // Service the socket right away to push the written data out in SSL mode. + return HandleWritable(); +} + +bool XmppSocketAdapter::Close() { + if (state_ == STATE_CLOSING) { + return false; // Avoid recursion, but not unexpected. + } + if (state_ == STATE_CLOSED) { + // In theory should not be trying to re-InternalClose. + SetError(ERROR_WRONGSTATE); + return false; + } + + // TODO(sync): deal with flushing close (flush, don't do reads, clean ssl). + + // If we've gotten to the point where we really do have a socket underneath + // then close it. It should call us back to tell us it is closed, and + // NotifyClose will be called. We indicate "closing" state so that we + // do not recusively try to keep closing the socket. + if (socket_) { + state_ = STATE_CLOSING; + socket_->Close(); + } + + // If we didn't get the callback, then we better make sure we signal + // closed. + if (state_ != STATE_CLOSED) { + // The socket was closed manually, not directly due to error. + if (error_ != ERROR_NONE) { + LOG(INFO) << "XmppSocketAdapter::Close - previous Error: " << error_ + << " WSAError: " << wsa_error_; + error_ = ERROR_NONE; + wsa_error_ = 0; + } + NotifyClose(); + } + return true; +} + +void XmppSocketAdapter::NotifyClose() { + if (state_ == STATE_CLOSED) { + SetError(ERROR_WRONGSTATE); + } else { + LOG(INFO) << "XmppSocketAdapter::NotifyClose - Error: " << error_ + << " WSAError: " << wsa_error_; + state_ = STATE_CLOSED; + SignalClosed(); + FreeState(); + } +} + +void XmppSocketAdapter::OnConnectEvent(talk_base::AsyncSocket *socket) { + if (state_ == STATE_CONNECTING) { + state_ = STATE_OPEN; + LOG(INFO) << "XmppSocketAdapter::OnConnectEvent - STATE_OPEN"; + SignalConnected(); +#if defined(FEATURE_ENABLE_SSL) + } else if (state_ == STATE_TLS_CONNECTING) { + state_ = STATE_TLS_OPEN; + LOG(INFO) << "XmppSocketAdapter::OnConnectEvent - STATE_TLS_OPEN"; + SignalSSLConnected(); + if (write_buffer_length_ > 0) { + HandleWritable(); + } +#endif // defined(FEATURE_ENABLE_SSL) + } else { + LOG(DFATAL) << "unexpected XmppSocketAdapter::OnConnectEvent state: " + << state_; + } +} + +void XmppSocketAdapter::OnReadEvent(talk_base::AsyncSocket *socket) { + HandleReadable(); +} + +void XmppSocketAdapter::OnWriteEvent(talk_base::AsyncSocket *socket) { + HandleWritable(); +} + +void XmppSocketAdapter::OnCloseEvent(talk_base::AsyncSocket *socket, + int error) { + LOG(INFO) << "XmppSocketAdapter::OnCloseEvent(" << error << ")"; + SetWSAError(error); + if (error == SOCKET_EACCES) { + SignalAuthenticationError(); // Proxy needs authentication. + } + NotifyClose(); +} + +#if defined(FEATURE_ENABLE_SSL) +bool XmppSocketAdapter::StartTls(const std::string& verify_host_name) { + if (state_ != STATE_OPEN) { + SetError(ERROR_WRONGSTATE); + return false; + } + + state_ = STATE_TLS_CONNECTING; + + DCHECK_EQ(write_buffer_length_, 0U); + + talk_base::SSLAdapter* ssl_adapter = + static_cast<talk_base::SSLAdapter*>(socket_); + + if (allow_unverified_certs_) { + ssl_adapter->set_ignore_bad_cert(true); + } + + if (ssl_adapter->StartSSL(verify_host_name.c_str(), false) != 0) { + state_ = STATE_OPEN; + SetError(ERROR_SSL); + return false; + } + + return true; +} +#endif // defined(FEATURE_ENABLE_SSL) + +void XmppSocketAdapter::QueueWriteData(const char* data, size_t len) { + // Expand buffer if needed. + if (write_buffer_length_ + len > write_buffer_capacity_) { + size_t new_capacity = 1024; + while (new_capacity < write_buffer_length_ + len) { + new_capacity = new_capacity * 2; + } + char* new_buffer = new char[new_capacity]; + DCHECK_LE(write_buffer_length_, 64000U); + memcpy(new_buffer, write_buffer_, write_buffer_length_); + delete[] write_buffer_; + write_buffer_ = new_buffer; + write_buffer_capacity_ = new_capacity; + } + + // Copy data into the end of buffer. + memcpy(write_buffer_ + write_buffer_length_, data, len); + write_buffer_length_ += len; +} + +void XmppSocketAdapter::FlushWriteQueue(Error* error, int* wsa_error) { + DCHECK(error); + DCHECK(wsa_error); + + size_t flushed = 0; + while (flushed < write_buffer_length_) { + int sent = socket_->Send(write_buffer_ + flushed, + static_cast<int>(write_buffer_length_ - flushed)); + if (sent < 0) { + if (!socket_->IsBlocking()) { + *error = ERROR_WINSOCK; + *wsa_error = socket_->GetError(); + } + break; + } + flushed += static_cast<size_t>(sent); + } + + // Remove flushed memory. + write_buffer_length_ -= flushed; + memmove(write_buffer_, write_buffer_ + flushed, write_buffer_length_); + + // When everything is flushed, deallocate the buffer if it's gotten big. + if (write_buffer_length_ == 0) { + if (write_buffer_capacity_ > 8192) { + delete[] write_buffer_; + write_buffer_ = NULL; + write_buffer_capacity_ = 0; + } + } +} + +void XmppSocketAdapter::SetError(Error error) { + if (error_ == ERROR_NONE) { + error_ = error; + } +} + +void XmppSocketAdapter::SetWSAError(int error) { + if (error_ == ERROR_NONE && error != 0) { + error_ = ERROR_WINSOCK; + wsa_error_ = error; + } +} + +bool XmppSocketAdapter::HandleReadable() { + if (!IsOpen()) + return false; + + SignalRead(); + return true; +} + +bool XmppSocketAdapter::HandleWritable() { + if (!IsOpen()) + return false; + + Error error = ERROR_NONE; + int wsa_error = 0; + FlushWriteQueue(&error, &wsa_error); + if (error != ERROR_NONE) { + Close(); + return false; + } + return true; +} + +} // namespace notifier diff --git a/jingle/notifier/communicator/xmpp_socket_adapter.h b/jingle/notifier/communicator/xmpp_socket_adapter.h new file mode 100644 index 0000000..ff22a9c --- /dev/null +++ b/jingle/notifier/communicator/xmpp_socket_adapter.h @@ -0,0 +1,87 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef JINGLE_NOTIFIER_COMMUNICATOR_XMPP_SOCKET_ADAPTER_H_ +#define JINGLE_NOTIFIER_COMMUNICATOR_XMPP_SOCKET_ADAPTER_H_ + +#include <string> + +#include "talk/base/asyncsocket.h" +#include "talk/xmpp/asyncsocket.h" +#include "talk/xmpp/xmppclientsettings.h" +#include "talk/xmpp/xmppengine.h" + +#ifndef _WIN32 +// Additional errors used by us from Win32 headers. +#define SEC_E_CERT_EXPIRED static_cast<int>(0x80090328L) +#define WSA_NOT_ENOUGH_MEMORY ENOMEM +#endif + +namespace notifier { + +class XmppSocketAdapter : public buzz::AsyncSocket, + public sigslot::has_slots<> { + public: + XmppSocketAdapter(const buzz::XmppClientSettings& xcs, + bool allow_unverified_certs); + virtual ~XmppSocketAdapter(); + + virtual State state() { return state_; } + virtual Error error() { return error_; } + virtual int GetError() { return wsa_error_; } + + void set_firewall(bool firewall) { firewall_ = firewall; } + + virtual bool Connect(const talk_base::SocketAddress& addr); + virtual bool Read(char* data, size_t len, size_t* len_read); + virtual bool Write(const char* data, size_t len); + virtual bool Close(); + +#if defined(FEATURE_ENABLE_SSL) + bool StartTls(const std::string& domainname); + bool IsOpen() const { return state_ == STATE_OPEN + || state_ == STATE_TLS_OPEN; } +#else + bool IsOpen() const { return state_ == STATE_OPEN; } +#endif + + sigslot::signal0<> SignalAuthenticationError; + + private: + // Return false if the socket is closed. + bool HandleReadable(); + bool HandleWritable(); + + State state_; + Error error_; + int wsa_error_; + + talk_base::AsyncSocket* socket_; + cricket::ProtocolType protocol_; + talk_base::ProxyInfo proxy_; + bool firewall_; + char* write_buffer_; + size_t write_buffer_length_; + size_t write_buffer_capacity_; + bool allow_unverified_certs_; + + bool FreeState(); + void NotifyClose(); + + void OnReadEvent(talk_base::AsyncSocket* socket); + void OnWriteEvent(talk_base::AsyncSocket* socket); + void OnConnectEvent(talk_base::AsyncSocket* socket); + void OnCloseEvent(talk_base::AsyncSocket* socket, int error); + + void QueueWriteData(const char* data, size_t len); + void FlushWriteQueue(Error* error, int* wsa_error); + + void SetError(Error error); + void SetWSAError(int error); + DISALLOW_COPY_AND_ASSIGN(XmppSocketAdapter); +}; + +} // namespace notifier + +#endif // JINGLE_NOTIFIER_COMMUNICATOR_XMPP_SOCKET_ADAPTER_H_ diff --git a/jingle/notifier/listener/listen_task.cc b/jingle/notifier/listener/listen_task.cc new file mode 100644 index 0000000..bfa79c1 --- /dev/null +++ b/jingle/notifier/listener/listen_task.cc @@ -0,0 +1,144 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "jingle/notifier/listener/listen_task.h" + +#include "base/logging.h" +#include "jingle/notifier/listener/notification_constants.h" +#include "jingle/notifier/listener/xml_element_util.h" +#include "talk/base/task.h" +#include "talk/xmllite/qname.h" +#include "talk/xmllite/xmlelement.h" +#include "talk/xmpp/xmppclient.h" +#include "talk/xmpp/constants.h" +#include "talk/xmpp/xmppengine.h" + +namespace notifier { + +ListenTask::ListenTask(Task* parent) + : buzz::XmppTask(parent, buzz::XmppEngine::HL_TYPE) { +} + +ListenTask::~ListenTask() { +} + +int ListenTask::ProcessStart() { + LOG(INFO) << "P2P: Listener task started."; + return STATE_RESPONSE; +} + +int ListenTask::ProcessResponse() { + LOG(INFO) << "P2P: Listener response received."; + const buzz::XmlElement* stanza = NextStanza(); + if (stanza == NULL) { + return STATE_BLOCKED; + } + // Acknowledge receipt of the notification to the buzz server. + scoped_ptr<buzz::XmlElement> response_stanza(MakeIqResult(stanza)); + SendStanza(response_stanza.get()); + + // TODO(akalin): Write unittests to cover this. + // Extract the service URL and service-specific data from the stanza. + // The response stanza has the following format. + // <iq from="{bare_jid}" to="{full_jid}" id="#" type="set"> + // <not:getAll xmlns:not="google:notifier"> + // <Timestamp long="#" xmlns=""/> + // <Result xmlns=""> + // <Id> + // <ServiceUrl data="{service_url}"/> + // <ServiceId data="{service_id}"/> + // </Id> + // <Timestamp long="#"/> + // <Content> + // <Priority int="#"/> + // <ServiceSpecificData data="{service_specific_data}"/> + // <RequireSubscription bool="true"/> + // </Content> + // <State> + // <Type int="#"/> + // <Read bool="{true/false}"/> + // </State> + // <ClientActive bool="{true/false}"/> + // </Result> + // </not:getAll> + // </iq> " + // Note that there can be multiple "Result" elements, so we need to loop + // through all of them. + bool update_signaled = false; + const buzz::XmlElement* get_all_element = + stanza->FirstNamed(buzz::QName("google:notifier", "getAll")); + if (get_all_element) { + const buzz::XmlElement* result_element = + get_all_element->FirstNamed( + buzz::QName(buzz::STR_EMPTY, "Result")); + while (result_element) { + IncomingNotificationData notification_data; + const buzz::XmlElement* id_element = + result_element->FirstNamed(buzz::QName(buzz::STR_EMPTY, "Id")); + if (id_element) { + const buzz::XmlElement* service_url_element = + id_element->FirstNamed( + buzz::QName(buzz::STR_EMPTY, "ServiceUrl")); + if (service_url_element) { + notification_data.service_url = service_url_element->Attr( + buzz::QName(buzz::STR_EMPTY, "data")); + } + } + const buzz::XmlElement* content_element = + result_element->FirstNamed( + buzz::QName(buzz::STR_EMPTY, "Content")); + if (content_element) { + const buzz::XmlElement* service_data_element = + content_element->FirstNamed( + buzz::QName(buzz::STR_EMPTY, "ServiceSpecificData")); + if (service_data_element) { + notification_data.service_specific_data = service_data_element->Attr( + buzz::QName(buzz::STR_EMPTY, "data")); + } + } + // Inform listeners that a notification has been received. + SignalUpdateAvailable(notification_data); + update_signaled = true; + // Now go to the next Result element + result_element = result_element->NextNamed( + buzz::QName(buzz::STR_EMPTY, "Result")); + } + } + if (!update_signaled) { + LOG(WARNING) << + "No getAll element or Result element found in response stanza"; + // Signal an empty update to preserve old behavior + SignalUpdateAvailable(IncomingNotificationData()); + } + return STATE_RESPONSE; +} + +bool ListenTask::HandleStanza(const buzz::XmlElement* stanza) { + LOG(INFO) << "P2P: Stanza received: " << XmlElementToString(*stanza); + // TODO(akalin): Do more verification on stanza depending on + // the sync notification method + if (IsValidNotification(stanza)) { + QueueStanza(stanza); + return true; + } + return false; +} + +bool ListenTask::IsValidNotification(const buzz::XmlElement* stanza) { + static const buzz::QName kQnNotifierGetAll( + kNotifierNamespace, "getAll"); + // An update notificaiton has the following form. + // <cli:iq from="{bare_jid}" to="{full_jid}" + // id="#" type="set" xmlns:cli="jabber:client"> + // <not:getAll xmlns:not="google:notifier"> + // <Timestamp long="#" xmlns=""/> + // </not:getAll> + // </cli:iq> + return + (MatchRequestIq(stanza, buzz::STR_SET, kQnNotifierGetAll) && + (stanza->Attr(buzz::QN_TO) == GetClient()->jid().Str()) && + (stanza->Attr(buzz::QN_FROM) == GetClient()->jid().BareJid().Str())); +} + +} // namespace notifier diff --git a/jingle/notifier/listener/listen_task.h b/jingle/notifier/listener/listen_task.h new file mode 100644 index 0000000..6a86775 --- /dev/null +++ b/jingle/notifier/listener/listen_task.h @@ -0,0 +1,49 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// This class listens for notifications from the talk service, and signals when +// they arrive. It checks all incoming stanza's to see if they look like +// notifications, and filters out those which are not valid. +// +// The task is deleted automatically by the buzz::XmppClient. This occurs in the +// destructor of TaskRunner, which is a superclass of buzz::XmppClient. + +#ifndef JINGLE_NOTIFIER_LISTENER_LISTEN_TASK_H_ +#define JINGLE_NOTIFIER_LISTENER_LISTEN_TASK_H_ + +#include "jingle/notifier/listener/notification_defines.h" +#include "talk/xmpp/xmpptask.h" + +namespace buzz { +class XmlElement; +class Jid; +} + +namespace notifier { + +class ListenTask : public buzz::XmppTask { + public: + explicit ListenTask(Task* parent); + virtual ~ListenTask(); + + // Overriden from buzz::XmppTask. + virtual int ProcessStart(); + virtual int ProcessResponse(); + virtual bool HandleStanza(const buzz::XmlElement* stanza); + + // Signal callback upon receipt of a notification. + // SignalUpdateAvailable(const IncomingNotificationData& data); + sigslot::signal1<const IncomingNotificationData&> SignalUpdateAvailable; + + private: + // Decide whether a notification should start a sync. We only validate that + // this notification came from our own Jid(). + bool IsValidNotification(const buzz::XmlElement* stanza); + + DISALLOW_COPY_AND_ASSIGN(ListenTask); +}; + +} // namespace notifier + +#endif // JINGLE_NOTIFIER_LISTENER_LISTEN_TASK_H_ diff --git a/jingle/notifier/listener/mediator_thread.h b/jingle/notifier/listener/mediator_thread.h new file mode 100644 index 0000000..643b1e5 --- /dev/null +++ b/jingle/notifier/listener/mediator_thread.h @@ -0,0 +1,55 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// These methods should post messages to a queue which a different thread will +// later come back and read from. + +#ifndef JINGLE_NOTIFIER_LISTENER_MEDIATOR_THREAD_H_ +#define JINGLE_NOTIFIER_LISTENER_MEDIATOR_THREAD_H_ + +#include <string> +#include <vector> + +#include "jingle/notifier/listener/notification_defines.h" + +namespace buzz { +class XmppClientSettings; +} // namespace buzz + +namespace notifier { + +class MediatorThread { + public: + virtual ~MediatorThread() {} + + class Delegate { + public: + virtual ~Delegate() {} + + virtual void OnConnectionStateChange(bool logged_in) = 0; + + virtual void OnSubscriptionStateChange(bool subscribed) = 0; + + virtual void OnIncomingNotification( + const IncomingNotificationData& notification_data) = 0; + + virtual void OnOutgoingNotification() = 0; + }; + + // |delegate| can be NULL if we're shutting down. + // TODO(akalin): Handle messages during shutdown gracefully so that + // we don't have to deal with NULL delegates. + virtual void SetDelegate(Delegate* delegate) = 0; + virtual void Login(const buzz::XmppClientSettings& settings) = 0; + virtual void Logout() = 0; + virtual void Start() = 0; + virtual void SubscribeForUpdates( + const std::vector<std::string>& subscribed_services_list) = 0; + virtual void ListenForUpdates() = 0; + virtual void SendNotification(const OutgoingNotificationData& data) = 0; +}; + +} // namespace notifier + +#endif // JINGLE_NOTIFIER_LISTENER_MEDIATOR_THREAD_H_ diff --git a/jingle/notifier/listener/mediator_thread_impl.cc b/jingle/notifier/listener/mediator_thread_impl.cc new file mode 100644 index 0000000..9f740dd --- /dev/null +++ b/jingle/notifier/listener/mediator_thread_impl.cc @@ -0,0 +1,368 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "jingle/notifier/listener/mediator_thread_impl.h" + +#include "base/logging.h" +#include "base/message_loop.h" +#include "base/task.h" +#include "jingle/notifier/base/task_pump.h" +#include "jingle/notifier/communicator/connection_options.h" +#include "jingle/notifier/communicator/const_communicator.h" +#include "jingle/notifier/communicator/xmpp_connection_generator.h" +#include "jingle/notifier/listener/listen_task.h" +#include "jingle/notifier/listener/send_update_task.h" +#include "jingle/notifier/listener/subscribe_task.h" +#include "net/base/host_port_pair.h" +#include "net/base/host_resolver.h" +#include "talk/base/physicalsocketserver.h" +#include "talk/base/thread.h" +#include "talk/xmpp/xmppclient.h" +#include "talk/xmpp/xmppclientsettings.h" + +// We manage the lifetime of notifier::MediatorThreadImpl ourselves. +DISABLE_RUNNABLE_METHOD_REFCOUNT(notifier::MediatorThreadImpl); + +namespace notifier { + +MediatorThreadImpl::MediatorThreadImpl() + : delegate_(NULL), + parent_message_loop_(MessageLoop::current()), + worker_thread_("MediatorThread worker thread") { + DCHECK(parent_message_loop_); +} + +MediatorThreadImpl::~MediatorThreadImpl() { + DCHECK_EQ(MessageLoop::current(), parent_message_loop_); +} + +void MediatorThreadImpl::SetDelegate(Delegate* delegate) { + DCHECK_EQ(MessageLoop::current(), parent_message_loop_); + delegate_ = delegate; +} + +void MediatorThreadImpl::Start() { + DCHECK_EQ(MessageLoop::current(), parent_message_loop_); + // We create the worker thread as an IO thread in preparation for + // making this use Chrome sockets. + const base::Thread::Options options(MessageLoop::TYPE_IO, 0); + // TODO(akalin): Make this function return a bool and remove this + // CHECK(). + CHECK(worker_thread_.StartWithOptions(options)); + worker_message_loop()->PostTask( + FROM_HERE, + NewRunnableMethod(this, &MediatorThreadImpl::StartLibjingleThread)); +} + +void MediatorThreadImpl::StartLibjingleThread() { + DCHECK_EQ(MessageLoop::current(), worker_message_loop()); + socket_server_.reset(new talk_base::PhysicalSocketServer()); + libjingle_thread_.reset(new talk_base::Thread()); + talk_base::ThreadManager::SetCurrent(libjingle_thread_.get()); + worker_message_loop()->PostTask( + FROM_HERE, + NewRunnableMethod(this, &MediatorThreadImpl::PumpLibjingleLoop)); +} + +void MediatorThreadImpl::StopLibjingleThread() { + DCHECK_EQ(MessageLoop::current(), worker_message_loop()); + talk_base::ThreadManager::SetCurrent(NULL); + libjingle_thread_.reset(); + socket_server_.reset(); +} + +void MediatorThreadImpl::PumpLibjingleLoop() { + DCHECK_EQ(MessageLoop::current(), worker_message_loop()); + // Pump the libjingle message loop 100ms at a time. + if (!libjingle_thread_.get()) { + // StopLibjingleThread() was called. + return; + } + libjingle_thread_->ProcessMessages(100); + worker_message_loop()->PostTask( + FROM_HERE, + NewRunnableMethod(this, &MediatorThreadImpl::PumpLibjingleLoop)); +} + +void MediatorThreadImpl::Login(const buzz::XmppClientSettings& settings) { + DCHECK_EQ(MessageLoop::current(), parent_message_loop_); + worker_message_loop()->PostTask( + FROM_HERE, + NewRunnableMethod(this, &MediatorThreadImpl::DoLogin, settings)); +} + +void MediatorThreadImpl::Logout() { + DCHECK_EQ(MessageLoop::current(), parent_message_loop_); + worker_message_loop()->PostTask( + FROM_HERE, + NewRunnableMethod(this, &MediatorThreadImpl::DoDisconnect)); + worker_message_loop()->PostTask( + FROM_HERE, + NewRunnableMethod(this, &MediatorThreadImpl::StopLibjingleThread)); + // TODO(akalin): Decomp this into a separate stop method. + worker_thread_.Stop(); + // Process any messages the worker thread may be posted on our + // thread. + bool old_state = parent_message_loop_->NestableTasksAllowed(); + parent_message_loop_->SetNestableTasksAllowed(true); + parent_message_loop_->RunAllPending(); + parent_message_loop_->SetNestableTasksAllowed(old_state); + // worker_thread_ should have cleaned all this up. + CHECK(!login_.get()); + CHECK(!pump_.get()); +} + +void MediatorThreadImpl::ListenForUpdates() { + DCHECK_EQ(MessageLoop::current(), parent_message_loop_); + worker_message_loop()->PostTask( + FROM_HERE, + NewRunnableMethod(this, &MediatorThreadImpl::DoListenForUpdates)); +} + +void MediatorThreadImpl::SubscribeForUpdates( + const std::vector<std::string>& subscribed_services_list) { + DCHECK_EQ(MessageLoop::current(), parent_message_loop_); + worker_message_loop()->PostTask( + FROM_HERE, + NewRunnableMethod(this, &MediatorThreadImpl::DoSubscribeForUpdates, + subscribed_services_list)); +} + +void MediatorThreadImpl::SendNotification( + const OutgoingNotificationData& data) { + DCHECK_EQ(MessageLoop::current(), parent_message_loop_); + worker_message_loop()->PostTask( + FROM_HERE, + NewRunnableMethod(this, &MediatorThreadImpl::DoSendNotification, + data)); +} + +MessageLoop* MediatorThreadImpl::worker_message_loop() { + MessageLoop* current_message_loop = MessageLoop::current(); + DCHECK(current_message_loop); + MessageLoop* worker_message_loop = worker_thread_.message_loop(); + DCHECK(worker_message_loop); + DCHECK(current_message_loop == parent_message_loop_ || + current_message_loop == worker_message_loop); + return worker_message_loop; +} + +buzz::XmppClient* MediatorThreadImpl::xmpp_client() { + DCHECK_EQ(MessageLoop::current(), worker_message_loop()); + DCHECK(login_.get()); + buzz::XmppClient* xmpp_client = login_->xmpp_client(); + DCHECK(xmpp_client); + return xmpp_client; +} + +void MediatorThreadImpl::DoLogin( + const buzz::XmppClientSettings& settings) { + DCHECK_EQ(MessageLoop::current(), worker_message_loop()); + LOG(INFO) << "P2P: Thread logging into talk network."; + + // TODO(akalin): Use an existing HostResolver from somewhere (maybe + // the IOThread one). + host_resolver_ = net::CreateSystemHostResolver(); + + // Start a new pump for the login. + login_.reset(); + pump_.reset(new notifier::TaskPump()); + + notifier::ServerInformation server_list[2]; + int server_list_count = 2; + + // The default servers know how to serve over port 443 (that's the magic). + server_list[0].server = net::HostPortPair("talk.google.com", + notifier::kDefaultXmppPort); + server_list[0].special_port_magic = true; + server_list[1].server = net::HostPortPair("talkx.l.google.com", + notifier::kDefaultXmppPort); + server_list[1].special_port_magic = true; + + // Autodetect proxy is on by default. + notifier::ConnectionOptions options; + + // Language is not used in the stanza so we default to |en|. + std::string lang = "en"; + login_.reset(new notifier::Login(pump_.get(), + settings, + options, + lang, + host_resolver_.get(), + server_list, + server_list_count, + // talk_base::FirewallManager* is NULL. + NULL, + // Both the proxy and a non-proxy route + // will be attempted. + false, + // |previous_login_successful| is true + // because we have already done a + // successful gaia login at this point + // through another mechanism. + true)); + + login_->SignalClientStateChange.connect( + this, &MediatorThreadImpl::OnClientStateChangeMessage); + login_->SignalLoginFailure.connect( + this, &MediatorThreadImpl::OnLoginFailureMessage); + login_->StartConnection(); +} + +void MediatorThreadImpl::DoDisconnect() { + DCHECK_EQ(MessageLoop::current(), worker_message_loop()); + LOG(INFO) << "P2P: Thread logging out of talk network."; + login_.reset(); + // Delete the old pump while on the thread to ensure that everything is + // cleaned-up in a predicatable manner. + pump_.reset(); + + host_resolver_ = NULL; +} + +void MediatorThreadImpl::DoSubscribeForUpdates( + const std::vector<std::string>& subscribed_services_list) { + DCHECK_EQ(MessageLoop::current(), worker_message_loop()); + SubscribeTask* subscription = + new SubscribeTask(xmpp_client(), subscribed_services_list); + subscription->SignalStatusUpdate.connect( + this, + &MediatorThreadImpl::OnSubscriptionStateChange); + subscription->Start(); +} + +void MediatorThreadImpl::DoListenForUpdates() { + DCHECK_EQ(MessageLoop::current(), worker_message_loop()); + ListenTask* listener = new ListenTask(xmpp_client()); + listener->SignalUpdateAvailable.connect( + this, + &MediatorThreadImpl::OnIncomingNotification); + listener->Start(); +} + +void MediatorThreadImpl::DoSendNotification( + const OutgoingNotificationData& data) { + DCHECK_EQ(MessageLoop::current(), worker_message_loop()); + SendUpdateTask* task = new SendUpdateTask(xmpp_client(), data); + task->SignalStatusUpdate.connect( + this, + &MediatorThreadImpl::OnOutgoingNotification); + task->Start(); +} + +void MediatorThreadImpl::OnIncomingNotification( + const IncomingNotificationData& notification_data) { + DCHECK_EQ(MessageLoop::current(), worker_message_loop()); + parent_message_loop_->PostTask( + FROM_HERE, + NewRunnableMethod( + this, + &MediatorThreadImpl::OnIncomingNotificationOnParentThread, + notification_data)); +} + +void MediatorThreadImpl::OnIncomingNotificationOnParentThread( + const IncomingNotificationData& notification_data) { + DCHECK_EQ(MessageLoop::current(), parent_message_loop_); + if (delegate_) { + delegate_->OnIncomingNotification(notification_data); + } +} + +void MediatorThreadImpl::OnOutgoingNotification(bool success) { + DCHECK_EQ(MessageLoop::current(), worker_message_loop()); + parent_message_loop_->PostTask( + FROM_HERE, + NewRunnableMethod( + this, + &MediatorThreadImpl::OnOutgoingNotificationOnParentThread, + success)); +} + +void MediatorThreadImpl::OnOutgoingNotificationOnParentThread( + bool success) { + DCHECK_EQ(MessageLoop::current(), parent_message_loop_); + if (delegate_ && success) { + delegate_->OnOutgoingNotification(); + } +} + +void MediatorThreadImpl::OnLoginFailureMessage( + const notifier::LoginFailure& failure) { + DCHECK_EQ(MessageLoop::current(), worker_message_loop()); + parent_message_loop_->PostTask( + FROM_HERE, + NewRunnableMethod( + this, + &MediatorThreadImpl::OnLoginFailureMessageOnParentThread, + failure)); +} + +void MediatorThreadImpl::OnLoginFailureMessageOnParentThread( + const notifier::LoginFailure& failure) { + DCHECK_EQ(MessageLoop::current(), parent_message_loop_); + if (delegate_) { + delegate_->OnConnectionStateChange(false); + } +} + +void MediatorThreadImpl::OnClientStateChangeMessage( + LoginConnectionState state) { + DCHECK_EQ(MessageLoop::current(), worker_message_loop()); + parent_message_loop_->PostTask( + FROM_HERE, + NewRunnableMethod( + this, + &MediatorThreadImpl::OnClientStateChangeMessageOnParentThread, + state)); +} + +void MediatorThreadImpl::OnClientStateChangeMessageOnParentThread( + LoginConnectionState state) { + DCHECK_EQ(MessageLoop::current(), parent_message_loop_); + switch (state) { + case STATE_CLOSED: + if (delegate_) { + delegate_->OnConnectionStateChange(false); + } + break; + case STATE_RETRYING: + case STATE_OPENING: + LOG(INFO) << "P2P: Thread trying to connect."; + // Maybe first time logon, maybe intermediate network disruption. Assume + // the server went down, and lost our subscription for updates. + if (delegate_) { + delegate_->OnSubscriptionStateChange(false); + } + break; + case STATE_OPENED: + if (delegate_) { + delegate_->OnConnectionStateChange(true); + } + break; + default: + LOG(WARNING) << "P2P: Unknown client state change."; + break; + } +} + +void MediatorThreadImpl::OnSubscriptionStateChange(bool success) { + DCHECK_EQ(MessageLoop::current(), worker_message_loop()); + parent_message_loop_->PostTask( + FROM_HERE, + NewRunnableMethod( + this, + &MediatorThreadImpl::OnSubscriptionStateChangeOnParentThread, + success)); +} + +void MediatorThreadImpl::OnSubscriptionStateChangeOnParentThread( + bool success) { + DCHECK_EQ(MessageLoop::current(), parent_message_loop_); + if (delegate_) { + delegate_->OnSubscriptionStateChange(success); + } +} + +} // namespace notifier diff --git a/jingle/notifier/listener/mediator_thread_impl.h b/jingle/notifier/listener/mediator_thread_impl.h new file mode 100644 index 0000000..0e2e22f --- /dev/null +++ b/jingle/notifier/listener/mediator_thread_impl.h @@ -0,0 +1,142 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// This object runs on a thread and knows how to interpret messages sent by the +// talk mediator. The mediator posts messages to a queue which the thread polls +// (in a super class). +// +// Example usage: +// +// MediatorThread m = new MediatorThreadImpl(pass in stuff); +// m.start(); // Start the thread +// // Once the thread is started, you can do server stuff. +// m.Login(loginInformation); +// // Events happen, the mediator finds out through its pump more messages +// // are dispatched to the thread eventually we want to log out. +// m.Logout(); +// delete m; // Also stops the thread. + +#ifndef JINGLE_NOTIFIER_LISTENER_MEDIATOR_THREAD_IMPL_H_ +#define JINGLE_NOTIFIER_LISTENER_MEDIATOR_THREAD_IMPL_H_ + +#include <string> +#include <vector> + +#include "base/logging.h" +#include "base/ref_counted.h" +#include "base/scoped_ptr.h" +#include "base/thread.h" +#include "jingle/notifier/communicator/login.h" +#include "jingle/notifier/communicator/login_connection_state.h" +#include "jingle/notifier/communicator/login_failure.h" +#include "jingle/notifier/listener/mediator_thread.h" +#include "talk/base/sigslot.h" +#include "talk/xmpp/xmppclientsettings.h" + +class MessageLoop; + +namespace buzz { +class XmppClient; +} // namespace buzz + +namespace net { +class HostResolver; +} // namespace net + +namespace notifier { +class TaskPump; +} // namespace notifier + +namespace talk_base { +class SocketServer; +class Thread; +} // namespace talk_base + +namespace notifier { + +class MediatorThreadImpl + : public MediatorThread, + public sigslot::has_slots<> { + public: + MediatorThreadImpl(); + virtual ~MediatorThreadImpl(); + + virtual void SetDelegate(Delegate* delegate); + + // Start the thread. + virtual void Start(); + + // These are called from outside threads, by the talk mediator object. + // They add messages to a queue which we poll in this thread. + virtual void Login(const buzz::XmppClientSettings& settings); + virtual void Logout(); + virtual void ListenForUpdates(); + virtual void SubscribeForUpdates( + const std::vector<std::string>& subscribed_services_list); + virtual void SendNotification(const OutgoingNotificationData& data); + + protected: + // Should only be called after Start(). + MessageLoop* worker_message_loop(); + + // Should only be called after OnConnectionStateChange() is called + // on the delegate with true. + buzz::XmppClient* xmpp_client(); + + Delegate* delegate_; + MessageLoop* parent_message_loop_; + + private: + void StartLibjingleThread(); + void PumpLibjingleLoop(); + void StopLibjingleThread(); + + // Called from within the thread on internal events. + void DoLogin(const buzz::XmppClientSettings& settings); + void DoDisconnect(); + void DoSubscribeForUpdates( + const std::vector<std::string>& subscribed_services_list); + void DoListenForUpdates(); + void DoSendNotification( + const OutgoingNotificationData& data); + + // These handle messages indicating an event happened in the outside + // world. These are all called from the worker thread. + void OnIncomingNotification( + const IncomingNotificationData& notification_data); + void OnOutgoingNotification(bool success); + void OnLoginFailureMessage(const notifier::LoginFailure& failure); + void OnClientStateChangeMessage(LoginConnectionState state); + void OnSubscriptionStateChange(bool success); + + // Equivalents of the above functions called from the parent thread. + void OnIncomingNotificationOnParentThread( + const IncomingNotificationData& notification_data); + void OnOutgoingNotificationOnParentThread(bool success); + void OnLoginFailureMessageOnParentThread( + const notifier::LoginFailure& failure); + void OnClientStateChangeMessageOnParentThread( + LoginConnectionState state); + void OnSubscriptionStateChangeOnParentThread( + bool success); + + base::Thread worker_thread_; + scoped_refptr<net::HostResolver> host_resolver_; + + // All buzz::XmppClients are owned by their parent. The root parent is the + // SingleLoginTask created by the notifier::Login object. This in turn is + // owned by the TaskPump. They are destroyed either when processing is + // complete or the pump shuts down. + scoped_ptr<notifier::TaskPump> pump_; + scoped_ptr<notifier::Login> login_; + + scoped_ptr<talk_base::SocketServer> socket_server_; + scoped_ptr<talk_base::Thread> libjingle_thread_; + + DISALLOW_COPY_AND_ASSIGN(MediatorThreadImpl); +}; + +} // namespace notifier + +#endif // JINGLE_NOTIFIER_LISTENER_MEDIATOR_THREAD_IMPL_H_ diff --git a/jingle/notifier/listener/mediator_thread_mock.h b/jingle/notifier/listener/mediator_thread_mock.h new file mode 100644 index 0000000..8a1eaec --- /dev/null +++ b/jingle/notifier/listener/mediator_thread_mock.h @@ -0,0 +1,97 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This is mock for delicious testing. +// It's very primitive, and it would have been better to use gmock, except +// that gmock is only for linux. + +#ifndef JINGLE_NOTIFIER_LISTENER_MEDIATOR_THREAD_MOCK_H_ +#define JINGLE_NOTIFIER_LISTENER_MEDIATOR_THREAD_MOCK_H_ + +#include <string> +#include <vector> + +#include "jingle/notifier/listener/mediator_thread.h" +#include "talk/xmpp/xmppclientsettings.h" + +namespace notifier { + +class MockMediatorThread : public MediatorThread { + public: + MockMediatorThread() : delegate_(NULL) { + Reset(); + } + + virtual ~MockMediatorThread() {} + + void Reset() { + login_calls = 0; + logout_calls = 0; + start_calls = 0; + subscribe_calls = 0; + listen_calls = 0; + send_calls = 0; + } + + virtual void SetDelegate(Delegate* delegate) { + delegate_ = delegate; + } + + // Overridden from MediatorThread + virtual void Login(const buzz::XmppClientSettings& settings) { + login_calls++; + if (delegate_) { + delegate_->OnConnectionStateChange(true); + } + } + + virtual void Logout() { + logout_calls++; + if (delegate_) { + delegate_->OnConnectionStateChange(false); + } + } + + virtual void Start() { + start_calls++; + } + + virtual void SubscribeForUpdates( + const std::vector<std::string>& subscribed_services_list) { + subscribe_calls++; + if (delegate_) { + delegate_->OnSubscriptionStateChange(true); + } + } + + virtual void ListenForUpdates() { + listen_calls++; + } + + virtual void SendNotification(const OutgoingNotificationData &) { + send_calls++; + if (delegate_) { + delegate_->OnOutgoingNotification(); + } + } + + void ReceiveNotification(const IncomingNotificationData& data) { + if (delegate_) { + delegate_->OnIncomingNotification(data); + } + } + + Delegate* delegate_; + // Intneral State + int login_calls; + int logout_calls; + int start_calls; + int subscribe_calls; + int listen_calls; + int send_calls; +}; + +} // namespace notifier + +#endif // JINGLE_NOTIFIER_LISTENER_MEDIATOR_THREAD_MOCK_H_ diff --git a/jingle/notifier/listener/notification_constants.cc b/jingle/notifier/listener/notification_constants.cc new file mode 100644 index 0000000..11e9157 --- /dev/null +++ b/jingle/notifier/listener/notification_constants.cc @@ -0,0 +1,11 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "jingle/notifier/listener/notification_constants.h" + +namespace notifier { + +const char kNotifierNamespace[] = "google:notifier"; + +} // namespace notifier diff --git a/jingle/notifier/listener/notification_constants.h b/jingle/notifier/listener/notification_constants.h new file mode 100644 index 0000000..04d9921 --- /dev/null +++ b/jingle/notifier/listener/notification_constants.h @@ -0,0 +1,14 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef JINGLE_NOTIFIER_LISTENER_NOTIFICATION_CONSTANTS_H_ +#define JINGLE_NOTIFIER_LISTENER_NOTIFICATION_CONSTANTS_H_ + +namespace notifier { + +extern const char kNotifierNamespace[]; + +} // namespace notifier + +#endif // JINGLE_NOTIFIER_LISTENER_NOTIFICATION_CONSTANTS_H_ diff --git a/jingle/notifier/listener/notification_defines.h b/jingle/notifier/listener/notification_defines.h new file mode 100644 index 0000000..8a42307 --- /dev/null +++ b/jingle/notifier/listener/notification_defines.h @@ -0,0 +1,34 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef JINGLE_NOTIFIER_LISTENER_NOTIFICATION_DEFINES_H_ +#define JINGLE_NOTIFIER_LISTENER_NOTIFICATION_DEFINES_H_ + +#include <string> + +struct IncomingNotificationData { + std::string service_url; + std::string service_specific_data; +}; + +struct OutgoingNotificationData { + OutgoingNotificationData() : send_content(false), priority(0), + require_subscription(false), + write_to_cache_only(false) { + } + // Id values + std::string service_url; + std::string service_id; + // This bool signifies whether the content fields should be + // sent with the outgoing data. + bool send_content; + // Content values. + std::string service_specific_data; + int priority; + bool require_subscription; + bool write_to_cache_only; +}; + +#endif // JINGLE_NOTIFIER_LISTENER_NOTIFICATION_DEFINES_H_ + diff --git a/jingle/notifier/listener/send_update_task.cc b/jingle/notifier/listener/send_update_task.cc new file mode 100644 index 0000000..e4b2884 --- /dev/null +++ b/jingle/notifier/listener/send_update_task.cc @@ -0,0 +1,131 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "jingle/notifier/listener/send_update_task.h" + +#include <string> + +#include "base/logging.h" +#include "base/scoped_ptr.h" +#include "jingle/notifier/listener/notification_constants.h" +#include "jingle/notifier/listener/xml_element_util.h" +#include "talk/xmllite/qname.h" +#include "talk/xmpp/xmppclient.h" +#include "talk/xmpp/constants.h" + +namespace notifier { + +SendUpdateTask::SendUpdateTask(TaskParent* parent, + const OutgoingNotificationData& data) + : XmppTask(parent, buzz::XmppEngine::HL_SINGLE), // Watch for one reply. + notification_data_(data) { +} + +SendUpdateTask::~SendUpdateTask() { +} + +bool SendUpdateTask::HandleStanza(const buzz::XmlElement* stanza) { + if (!MatchResponseIq(stanza, GetClient()->jid().BareJid(), task_id())) + return false; + QueueStanza(stanza); + return true; +} + +int SendUpdateTask::ProcessStart() { + LOG(INFO) << "P2P: Notification task started."; + scoped_ptr<buzz::XmlElement> stanza( + MakeUpdateMessage(notification_data_, + GetClient()->jid().BareJid(), task_id())); + LOG(INFO) << "P2P: Notification stanza: " + << XmlElementToString(*stanza.get()); + + if (SendStanza(stanza.get()) != buzz::XMPP_RETURN_OK) { + // TODO(brg) : Retry on error. + // TODO(akalin): Or maybe immediately return STATE_ERROR and let + // retries happen a higher level. In any case, STATE_ERROR should + // eventually be returned. + SignalStatusUpdate(false); + return STATE_DONE; + } + return STATE_RESPONSE; +} + +int SendUpdateTask::ProcessResponse() { + LOG(INFO) << "P2P: Notification response received."; + const buzz::XmlElement* stanza = NextStanza(); + if (stanza == NULL) { + return STATE_BLOCKED; + } + LOG(INFO) << "P2P: Notification response: " << XmlElementToString(*stanza); + if (stanza->HasAttr(buzz::QN_TYPE) && + stanza->Attr(buzz::QN_TYPE) == buzz::STR_RESULT) { + // Notify listeners of success. + SignalStatusUpdate(true); + return STATE_DONE; + } + + // An error response was received. + // TODO(brg) : Error handling. + SignalStatusUpdate(false); + // TODO(akalin): This should be STATE_ERROR. + return STATE_DONE; +} + +buzz::XmlElement* SendUpdateTask::MakeUpdateMessage( + const OutgoingNotificationData& notification_data, + const buzz::Jid& to_jid_bare, const std::string& task_id) { + DCHECK(to_jid_bare.IsBare()); + static const buzz::QName kQnNotifierSet(kNotifierNamespace, "set"); + static const buzz::QName kQnId(buzz::STR_EMPTY, "Id"); + static const buzz::QName kQnContent(buzz::STR_EMPTY, "Content"); + + // Create our update stanza. The message is constructed as: + // <iq type='get' from='{fullJid}' to='{bareJid}' id='{#}'> + // <gn:set xmlns:gn="google:notifier" xmlns=""> + // <Id> + // <ServiceUrl data="{Service_Url}" /> + // <ServiceId data="{Service_Id}" /> + // </Id> + // If content needs to be sent, then the below element is added + // <Content> + // <Priority int="{Priority}" /> + // <RequireSubscription bool="{true/false}" /> + // <!-- If is_transitional is set, this is omitted. --> + // <ServiceSpecificData data="{ServiceData}" /> + // <WriteToCacheOnly bool="{true/false}" /> + // </Content> + // </set> + // </iq> + buzz::XmlElement* iq = MakeIq(buzz::STR_GET, to_jid_bare, task_id); + buzz::XmlElement* set = new buzz::XmlElement(kQnNotifierSet, true); + buzz::XmlElement* id = new buzz::XmlElement(kQnId, true); + iq->AddElement(set); + set->AddElement(id); + + id->AddElement(MakeStringXmlElement("ServiceUrl", + notification_data.service_url.c_str())); + id->AddElement(MakeStringXmlElement("ServiceId", + notification_data.service_id.c_str())); + + if (notification_data.send_content) { + buzz::XmlElement* content = new buzz::XmlElement(kQnContent, true); + set->AddElement(content); + content->AddElement(MakeIntXmlElement("Priority", + notification_data.priority)); + content->AddElement( + MakeBoolXmlElement("RequireSubscription", + notification_data.require_subscription)); + if (!notification_data.service_specific_data.empty()) { + content->AddElement( + MakeStringXmlElement("ServiceSpecificData", + notification_data.service_specific_data.c_str())); + } + content->AddElement( + MakeBoolXmlElement("WriteToCacheOnly", + notification_data.write_to_cache_only)); + } + return iq; +} + +} // namespace notifier diff --git a/jingle/notifier/listener/send_update_task.h b/jingle/notifier/listener/send_update_task.h new file mode 100644 index 0000000..03285dd --- /dev/null +++ b/jingle/notifier/listener/send_update_task.h @@ -0,0 +1,47 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Methods for sending the update stanza to notify peers via xmpp. + +#ifndef JINGLE_NOTIFIER_LISTENER_SEND_UPDATE_TASK_H_ +#define JINGLE_NOTIFIER_LISTENER_SEND_UPDATE_TASK_H_ + +#include <string> + +#include "jingle/notifier/listener/notification_defines.h" +#include "talk/xmllite/xmlelement.h" +#include "talk/xmpp/xmpptask.h" +#include "testing/gtest/include/gtest/gtest_prod.h" + +namespace notifier { + +class SendUpdateTask : public buzz::XmppTask { + public: + SendUpdateTask(TaskParent* parent, const OutgoingNotificationData& data); + virtual ~SendUpdateTask(); + + // Overridden from buzz::XmppTask. + virtual int ProcessStart(); + virtual int ProcessResponse(); + virtual bool HandleStanza(const buzz::XmlElement* stanza); + + // Signal callback upon subscription success. + sigslot::signal1<bool> SignalStatusUpdate; + + private: + // Allocates and constructs an buzz::XmlElement containing the update stanza. + static buzz::XmlElement* MakeUpdateMessage( + const OutgoingNotificationData& notification_data, + const buzz::Jid& to_jid_bare, const std::string& task_id); + + OutgoingNotificationData notification_data_; + + FRIEND_TEST(SendUpdateTaskTest, MakeUpdateMessage); + + DISALLOW_COPY_AND_ASSIGN(SendUpdateTask); +}; + +} // namespace notifier + +#endif // JINGLE_NOTIFIER_LISTENER_SEND_UPDATE_TASK_H_ diff --git a/jingle/notifier/listener/send_update_task_unittest.cc b/jingle/notifier/listener/send_update_task_unittest.cc new file mode 100644 index 0000000..80a87aa --- /dev/null +++ b/jingle/notifier/listener/send_update_task_unittest.cc @@ -0,0 +1,116 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "jingle/notifier/listener/send_update_task.h" + +#include "base/logging.h" +#include "base/scoped_ptr.h" +#include "base/string_util.h" +#include "jingle/notifier/listener/xml_element_util.h" +#include "talk/xmpp/jid.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace buzz { +class XmlElement; +} + +namespace notifier { + +class SendUpdateTaskTest : public testing::Test { + public: + SendUpdateTaskTest() : to_jid_bare_("to@jid.com"), task_id_("taskid") { + EXPECT_EQ(to_jid_bare_.Str(), to_jid_bare_.BareJid().Str()); + } + + protected: + const buzz::Jid to_jid_bare_; + const std::string task_id_; + + private: + DISALLOW_COPY_AND_ASSIGN(SendUpdateTaskTest); +}; + +TEST_F(SendUpdateTaskTest, MakeUpdateMessage) { + OutgoingNotificationData data; + data.service_id = "test_service_id"; + data.service_url = "test_service_url"; + data.send_content = false; + data.priority = 200; + data.write_to_cache_only = true; + data.require_subscription = false; + + scoped_ptr<buzz::XmlElement> message_without_content( + SendUpdateTask::MakeUpdateMessage(data, to_jid_bare_, task_id_)); + + std::string expected_xml_string = + StringPrintf( + "<cli:iq type=\"get\" to=\"%s\" id=\"%s\" " + "xmlns:cli=\"jabber:client\">" + "<set xmlns=\"google:notifier\">" + "<Id xmlns=\"\">" + "<ServiceUrl xmlns=\"\" data=\"test_service_url\"/>" + "<ServiceId xmlns=\"\" data=\"test_service_id\"/>" + "</Id>" + "</set>" + "</cli:iq>", + to_jid_bare_.Str().c_str(), task_id_.c_str()); + EXPECT_EQ(expected_xml_string, XmlElementToString(*message_without_content)); + + data.send_content = true; + + expected_xml_string = + StringPrintf( + "<cli:iq type=\"get\" to=\"%s\" id=\"%s\" " + "xmlns:cli=\"jabber:client\">" + "<set xmlns=\"google:notifier\">" + "<Id xmlns=\"\">" + "<ServiceUrl xmlns=\"\" " + "data=\"test_service_url\"/>" + "<ServiceId xmlns=\"\" data=\"test_service_id\"/>" + "</Id>" + "<Content xmlns=\"\">" + "<Priority xmlns=\"\" int=\"200\"/>" + "<RequireSubscription xmlns=\"\" bool=\"false\"/>" + "<WriteToCacheOnly xmlns=\"\" bool=\"true\"/>" + "</Content>" + "</set>" + "</cli:iq>", + to_jid_bare_.Str().c_str(), task_id_.c_str()); + + scoped_ptr<buzz::XmlElement> message_with_content( + SendUpdateTask::MakeUpdateMessage(data, to_jid_bare_, task_id_)); + + EXPECT_EQ(expected_xml_string, XmlElementToString(*message_with_content)); + + data.service_specific_data = "test_service_specific_data"; + data.require_subscription = true; + + expected_xml_string = + StringPrintf( + "<cli:iq type=\"get\" to=\"%s\" id=\"%s\" " + "xmlns:cli=\"jabber:client\">" + "<set xmlns=\"google:notifier\">" + "<Id xmlns=\"\">" + "<ServiceUrl xmlns=\"\" " + "data=\"test_service_url\"/>" + "<ServiceId xmlns=\"\" data=\"test_service_id\"/>" + "</Id>" + "<Content xmlns=\"\">" + "<Priority xmlns=\"\" int=\"200\"/>" + "<RequireSubscription xmlns=\"\" bool=\"true\"/>" + "<ServiceSpecificData xmlns=\"\" " + "data=\"test_service_specific_data\"/>" + "<WriteToCacheOnly xmlns=\"\" bool=\"true\"/>" + "</Content>" + "</set>" + "</cli:iq>", + to_jid_bare_.Str().c_str(), task_id_.c_str()); + + scoped_ptr<buzz::XmlElement> message_with_data( + SendUpdateTask::MakeUpdateMessage(data, to_jid_bare_, task_id_)); + + EXPECT_EQ(expected_xml_string, XmlElementToString(*message_with_data)); +} + +} // namespace notifier diff --git a/jingle/notifier/listener/subscribe_task.cc b/jingle/notifier/listener/subscribe_task.cc new file mode 100644 index 0000000..7636b48 --- /dev/null +++ b/jingle/notifier/listener/subscribe_task.cc @@ -0,0 +1,109 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "jingle/notifier/listener/subscribe_task.h" + +#include <string> + +#include "base/logging.h" +#include "jingle/notifier/listener/notification_constants.h" +#include "jingle/notifier/listener/xml_element_util.h" +#include "talk/base/task.h" +#include "talk/xmllite/qname.h" +#include "talk/xmllite/xmlelement.h" +#include "talk/xmpp/xmppclient.h" +#include "talk/xmpp/constants.h" +#include "talk/xmpp/xmppengine.h" + +namespace notifier { + +SubscribeTask::SubscribeTask( + TaskParent* parent, + const std::vector<std::string>& subscribed_services_list) + : XmppTask(parent, buzz::XmppEngine::HL_SINGLE), + subscribed_services_list_(subscribed_services_list) { +} + +SubscribeTask::~SubscribeTask() { +} + +bool SubscribeTask::HandleStanza(const buzz::XmlElement* stanza) { + if (!MatchResponseIq(stanza, GetClient()->jid().BareJid(), task_id())) + return false; + QueueStanza(stanza); + return true; +} + +int SubscribeTask::ProcessStart() { + LOG(INFO) << "P2P: Subscription task started."; + scoped_ptr<buzz::XmlElement> iq_stanza( + MakeSubscriptionMessage(subscribed_services_list_, + GetClient()->jid().BareJid(), task_id())); + LOG(INFO) << "P2P: Subscription stanza: " + << XmlElementToString(*iq_stanza.get()); + + if (SendStanza(iq_stanza.get()) != buzz::XMPP_RETURN_OK) { + SignalStatusUpdate(false); + // TODO(akalin): This should be STATE_ERROR. + return STATE_DONE; + } + return STATE_RESPONSE; +} + +int SubscribeTask::ProcessResponse() { + LOG(INFO) << "P2P: Subscription response received."; + const buzz::XmlElement* stanza = NextStanza(); + if (stanza == NULL) { + return STATE_BLOCKED; + } + LOG(INFO) << "P2P: Subscription response: " << XmlElementToString(*stanza); + // We've receieved a response to our subscription request. + if (stanza->HasAttr(buzz::QN_TYPE) && + stanza->Attr(buzz::QN_TYPE) == buzz::STR_RESULT) { + SignalStatusUpdate(true); + return STATE_DONE; + } + // An error response was received. + // TODO(brg) : Error handling. + SignalStatusUpdate(false); + // TODO(akalin): This should be STATE_ERROR. + return STATE_DONE; +} + +buzz::XmlElement* SubscribeTask::MakeSubscriptionMessage( + const std::vector<std::string>& subscribed_services_list, + const buzz::Jid& to_jid_bare, const std::string& task_id) { + DCHECK(to_jid_bare.IsBare()); + static const buzz::QName kQnNotifierGetAll( + kNotifierNamespace, "getAll"); + + // Create the subscription stanza using the notifications protocol. + // <iq type='get' from='{fullJid}' to='{bareJid}' id='{#}'> + // <gn:getAll xmlns:gn="google:notifier" xmlns=""> + // <ClientActive bool="true" /> + // <!-- present only if subscribed_services_list is not empty --> + // <SubscribedServiceUrl data="google:notifier"> + // <SubscribedServiceUrl data="http://www.google.com/chrome/sync"> + // <FilterNonSubscribed bool="true" /> + // </gn:getAll> + // </iq> + + buzz::XmlElement* iq = MakeIq(buzz::STR_GET, to_jid_bare, task_id); + buzz::XmlElement* get_all = new buzz::XmlElement(kQnNotifierGetAll, true); + iq->AddElement(get_all); + + get_all->AddElement(MakeBoolXmlElement("ClientActive", true)); + for (std::vector<std::string>::const_iterator iter = + subscribed_services_list.begin(); + iter != subscribed_services_list.end(); ++iter) { + get_all->AddElement( + MakeStringXmlElement("SubscribedServiceUrl", iter->c_str())); + } + if (!subscribed_services_list.empty()) { + get_all->AddElement(MakeBoolXmlElement("FilterNonSubscribed", true)); + } + return iq; +} + +} // namespace notifier diff --git a/jingle/notifier/listener/subscribe_task.h b/jingle/notifier/listener/subscribe_task.h new file mode 100644 index 0000000..5b344d9 --- /dev/null +++ b/jingle/notifier/listener/subscribe_task.h @@ -0,0 +1,51 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// This class handles subscribing to talk notifications. It does the getAll iq +// stanza which establishes the endpoint and directs future notifications to be +// pushed. + +#ifndef JINGLE_NOTIFIER_LISTENER_SUBSCRIBE_TASK_H_ +#define JINGLE_NOTIFIER_LISTENER_SUBSCRIBE_TASK_H_ + +#include <string> +#include <vector> + +#include "talk/xmllite/xmlelement.h" +#include "talk/xmpp/xmpptask.h" +#include "testing/gtest/include/gtest/gtest_prod.h" + +namespace notifier { +// TODO(akalin): Remove NOTIFICATION_LEGACY and remove/refactor relevant code +// in this class and any other class that uses notification_method. +class SubscribeTask : public buzz::XmppTask { + public: + SubscribeTask(TaskParent* parent, + const std::vector<std::string>& subscribed_services_list); + virtual ~SubscribeTask(); + + // Overridden from XmppTask. + virtual int ProcessStart(); + virtual int ProcessResponse(); + virtual bool HandleStanza(const buzz::XmlElement* stanza); + + // Signal callback upon subscription success. + sigslot::signal1<bool> SignalStatusUpdate; + + private: + // Assembles an Xmpp stanza which can be sent to subscribe to notifications. + static buzz::XmlElement* MakeSubscriptionMessage( + const std::vector<std::string>& subscribed_services_list, + const buzz::Jid& to_jid_bare, const std::string& task_id); + + std::vector<std::string> subscribed_services_list_; + + FRIEND_TEST(SubscribeTaskTest, MakeSubscriptionMessage); + + DISALLOW_COPY_AND_ASSIGN(SubscribeTask); +}; + +} // namespace notifier + +#endif // JINGLE_NOTIFIER_LISTENER_SUBSCRIBE_TASK_H_ diff --git a/jingle/notifier/listener/subscribe_task_unittest.cc b/jingle/notifier/listener/subscribe_task_unittest.cc new file mode 100644 index 0000000..3b44fd8 --- /dev/null +++ b/jingle/notifier/listener/subscribe_task_unittest.cc @@ -0,0 +1,74 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "jingle/notifier/listener/subscribe_task.h" + +#include "base/logging.h" +#include "base/scoped_ptr.h" +#include "base/string_util.h" +#include "jingle/notifier/listener/xml_element_util.h" +#include "talk/xmpp/jid.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace buzz { +class XmlElement; +} + +namespace notifier { + +class SubscribeTaskTest : public testing::Test { + public: + SubscribeTaskTest() : to_jid_bare_("to@jid.com"), task_id_("taskid") { + EXPECT_EQ(to_jid_bare_.Str(), to_jid_bare_.BareJid().Str()); + } + + protected: + const buzz::Jid to_jid_bare_; + const std::string task_id_; + + private: + DISALLOW_COPY_AND_ASSIGN(SubscribeTaskTest); +}; + +TEST_F(SubscribeTaskTest, MakeSubscriptionMessage) { + std::vector<std::string> subscribed_services_list; + + scoped_ptr<buzz::XmlElement> message_without_services( + SubscribeTask::MakeSubscriptionMessage(subscribed_services_list, + to_jid_bare_, task_id_)); + std::string expected_xml_string = + StringPrintf( + "<cli:iq type=\"get\" to=\"%s\" id=\"%s\" " + "xmlns:cli=\"jabber:client\">" + "<getAll xmlns=\"google:notifier\">" + "<ClientActive xmlns=\"\" bool=\"true\"/>" + "</getAll>" + "</cli:iq>", + to_jid_bare_.Str().c_str(), task_id_.c_str()); + EXPECT_EQ(expected_xml_string, XmlElementToString(*message_without_services)); + + subscribed_services_list.push_back("test_service_url1"); + subscribed_services_list.push_back("test_service_url2"); + scoped_ptr<buzz::XmlElement> message_with_services( + SubscribeTask::MakeSubscriptionMessage(subscribed_services_list, + to_jid_bare_, task_id_)); + expected_xml_string = + StringPrintf( + "<cli:iq type=\"get\" to=\"%s\" id=\"%s\" " + "xmlns:cli=\"jabber:client\">" + "<getAll xmlns=\"google:notifier\">" + "<ClientActive xmlns=\"\" bool=\"true\"/>" + "<SubscribedServiceUrl " + "xmlns=\"\" data=\"test_service_url1\"/>" + "<SubscribedServiceUrl " + "xmlns=\"\" data=\"test_service_url2\"/>" + "<FilterNonSubscribed xmlns=\"\" bool=\"true\"/>" + "</getAll>" + "</cli:iq>", + to_jid_bare_.Str().c_str(), task_id_.c_str()); + + EXPECT_EQ(expected_xml_string, XmlElementToString(*message_with_services)); +} + +} // namespace notifier diff --git a/jingle/notifier/listener/talk_mediator.h b/jingle/notifier/listener/talk_mediator.h new file mode 100644 index 0000000..dfdc260 --- /dev/null +++ b/jingle/notifier/listener/talk_mediator.h @@ -0,0 +1,63 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Interface to the code which handles talk logic. Used to initialize SSL +// before the underlying talk login occurs. +// +// Example usage: +// +// TalkMediator mediator(); +// mediator.SetAuthToken("email", "token", "service_id"); +// mediator.Login(); +// ... +// mediator.Logout(); + +#ifndef JINGLE_NOTIFIER_LISTENER_TALK_MEDIATOR_H_ +#define JINGLE_NOTIFIER_LISTENER_TALK_MEDIATOR_H_ + +#include <string> + +#include "jingle/notifier/listener/notification_defines.h" + +namespace notifier { + +class TalkMediator { + public: + TalkMediator() {} + virtual ~TalkMediator() {} + + class Delegate { + public: + virtual ~Delegate() {} + + virtual void OnNotificationStateChange( + bool notifications_enabled) = 0; + + virtual void OnIncomingNotification( + const IncomingNotificationData& notification_data) = 0; + + virtual void OnOutgoingNotification() = 0; + }; + + // |delegate| may be NULL. + virtual void SetDelegate(Delegate* delegate) = 0; + + // The following methods are for authorizaiton of the xmpp client. + virtual bool SetAuthToken(const std::string& email, + const std::string& token, + const std::string& token_service) = 0; + virtual bool Login() = 0; + virtual bool Logout() = 0; + + // Method for the owner of this object to notify peers that an update has + // occurred. + virtual bool SendNotification(const OutgoingNotificationData& data) = 0; + + // Add a URL to subscribe to for notifications. + virtual void AddSubscribedServiceUrl(const std::string& service_url) = 0; +}; + +} // namespace notifier + +#endif // JINGLE_NOTIFIER_LISTENER_TALK_MEDIATOR_H_ diff --git a/jingle/notifier/listener/talk_mediator_impl.cc b/jingle/notifier/listener/talk_mediator_impl.cc new file mode 100644 index 0000000..3276778 --- /dev/null +++ b/jingle/notifier/listener/talk_mediator_impl.cc @@ -0,0 +1,195 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "jingle/notifier/listener/talk_mediator_impl.h" + +#include "base/logging.h" +#include "base/singleton.h" +#include "jingle/notifier/listener/mediator_thread_impl.h" +#include "talk/base/cryptstring.h" +#include "talk/base/ssladapter.h" +#include "talk/xmpp/xmppclientsettings.h" +#include "talk/xmpp/xmppengine.h" + +namespace notifier { + +// Before any authorization event from TalkMediatorImpl, we need to initialize +// the SSL library. +class SslInitializationSingleton { + public: + virtual ~SslInitializationSingleton() { + talk_base::CleanupSSL(); + }; + + void RegisterClient() {} + + static SslInitializationSingleton* GetInstance() { + return Singleton<SslInitializationSingleton>::get(); + } + + private: + friend struct DefaultSingletonTraits<SslInitializationSingleton>; + + SslInitializationSingleton() { + talk_base::InitializeSSL(); + }; + + DISALLOW_COPY_AND_ASSIGN(SslInitializationSingleton); +}; + +TalkMediatorImpl::TalkMediatorImpl( + MediatorThread* mediator_thread, + bool initialize_ssl, + bool connect_immediately, + bool invalidate_xmpp_auth_token) + : delegate_(NULL), + mediator_thread_(mediator_thread), + invalidate_xmpp_auth_token_(invalidate_xmpp_auth_token) { + DCHECK(non_thread_safe_.CalledOnValidThread()); + if (initialize_ssl) { + SslInitializationSingleton::GetInstance()->RegisterClient(); + } + if (connect_immediately) { + mediator_thread_->SetDelegate(this); + state_.connected = 1; + } + mediator_thread_->Start(); + state_.started = 1; +} + +TalkMediatorImpl::~TalkMediatorImpl() { + DCHECK(non_thread_safe_.CalledOnValidThread()); + if (state_.started) { + Logout(); + } +} + +bool TalkMediatorImpl::Login() { + DCHECK(non_thread_safe_.CalledOnValidThread()); + // Connect to the mediator thread and start processing messages. + if (!state_.connected) { + mediator_thread_->SetDelegate(this); + state_.connected = 1; + } + if (state_.initialized && !state_.logging_in && !state_.logged_in) { + state_.logging_in = true; + mediator_thread_->Login(xmpp_settings_); + return true; + } + return false; +} + +bool TalkMediatorImpl::Logout() { + DCHECK(non_thread_safe_.CalledOnValidThread()); + if (state_.connected) { + state_.connected = 0; + } + if (state_.started) { + state_.started = 0; + state_.logging_in = 0; + state_.logged_in = 0; + state_.subscribed = 0; + // We do not want to be called back during logout since we may be + // closing. + mediator_thread_->SetDelegate(NULL); + mediator_thread_->Logout(); + return true; + } + return false; +} + +bool TalkMediatorImpl::SendNotification(const OutgoingNotificationData& data) { + DCHECK(non_thread_safe_.CalledOnValidThread()); + if (state_.logged_in && state_.subscribed) { + mediator_thread_->SendNotification(data); + return true; + } + return false; +} + +void TalkMediatorImpl::SetDelegate(TalkMediator::Delegate* delegate) { + DCHECK(non_thread_safe_.CalledOnValidThread()); + delegate_ = delegate; +} + +bool TalkMediatorImpl::SetAuthToken(const std::string& email, + const std::string& token, + const std::string& token_service) { + DCHECK(non_thread_safe_.CalledOnValidThread()); + + // Verify that we can create a JID from the email provided. + buzz::Jid jid = buzz::Jid(email); + if (jid.node().empty() || !jid.IsValid()) { + return false; + } + + // Construct the XmppSettings object for login to buzz. + xmpp_settings_.set_user(jid.node()); + xmpp_settings_.set_resource("chrome-sync"); + xmpp_settings_.set_host(jid.domain()); + xmpp_settings_.set_use_tls(true); + xmpp_settings_.set_auth_cookie(invalidate_xmpp_auth_token_ ? + token + "bogus" : token); + xmpp_settings_.set_token_service(token_service); + + state_.initialized = 1; + return true; +} + +void TalkMediatorImpl::AddSubscribedServiceUrl( + const std::string& service_url) { + DCHECK(non_thread_safe_.CalledOnValidThread()); + subscribed_services_list_.push_back(service_url); + if (state_.logged_in) { + LOG(INFO) << "Resubscribing for updates, a new service got added"; + mediator_thread_->SubscribeForUpdates(subscribed_services_list_); + } +} + + +void TalkMediatorImpl::OnConnectionStateChange(bool logged_in) { + DCHECK(non_thread_safe_.CalledOnValidThread()); + state_.logging_in = 0; + state_.logged_in = logged_in; + if (logged_in) { + LOG(INFO) << "P2P: Logged in."; + // ListenForUpdates enables the ListenTask. This is done before + // SubscribeForUpdates. + mediator_thread_->ListenForUpdates(); + // Now subscribe for updates to all the services we are interested in + mediator_thread_->SubscribeForUpdates(subscribed_services_list_); + } else { + LOG(INFO) << "P2P: Logged off."; + OnSubscriptionStateChange(false); + } +} + +void TalkMediatorImpl::OnSubscriptionStateChange(bool subscribed) { + DCHECK(non_thread_safe_.CalledOnValidThread()); + state_.subscribed = subscribed; + LOG(INFO) << "P2P: " << (subscribed ? "subscribed" : "unsubscribed"); + if (delegate_) { + delegate_->OnNotificationStateChange(subscribed); + } +} + +void TalkMediatorImpl::OnIncomingNotification( + const IncomingNotificationData& notification_data) { + DCHECK(non_thread_safe_.CalledOnValidThread()); + LOG(INFO) << "P2P: Updates are available on the server."; + if (delegate_) { + delegate_->OnIncomingNotification(notification_data); + } +} + +void TalkMediatorImpl::OnOutgoingNotification() { + DCHECK(non_thread_safe_.CalledOnValidThread()); + LOG(INFO) << + "P2P: Peers were notified that updates are available on the server."; + if (delegate_) { + delegate_->OnOutgoingNotification(); + } +} + +} // namespace notifier diff --git a/jingle/notifier/listener/talk_mediator_impl.h b/jingle/notifier/listener/talk_mediator_impl.h new file mode 100644 index 0000000..e6fc988 --- /dev/null +++ b/jingle/notifier/listener/talk_mediator_impl.h @@ -0,0 +1,104 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// This class is the interface between talk code and the client code proper +// It will manage all aspects of the connection and call back into the client +// when it needs attention (for instance if updates are available for syncing). + +#ifndef JINGLE_NOTIFIER_LISTENER_TALK_MEDIATOR_IMPL_H_ +#define JINGLE_NOTIFIER_LISTENER_TALK_MEDIATOR_IMPL_H_ + +#include <string> +#include <vector> + +#include "base/non_thread_safe.h" +#include "base/scoped_ptr.h" +#include "jingle/notifier/listener/mediator_thread.h" +#include "jingle/notifier/listener/talk_mediator.h" +#include "talk/base/sigslot.h" +#include "talk/xmpp/xmppclientsettings.h" +#include "testing/gtest/include/gtest/gtest_prod.h" // For FRIEND_TEST + +namespace notifier { + +class TalkMediatorImpl + : public TalkMediator, public MediatorThread::Delegate { + public: + // Takes ownership of |mediator_thread|. + TalkMediatorImpl( + MediatorThread* mediator_thread, + bool initialize_ssl, + bool connect_immediately, + bool invalidate_xmpp_auth_token); + virtual ~TalkMediatorImpl(); + + // TalkMediator implementation. + + virtual void SetDelegate(TalkMediator::Delegate* delegate); + + virtual bool SetAuthToken(const std::string& email, + const std::string& token, + const std::string& token_service); + virtual bool Login(); + virtual bool Logout(); + + virtual bool SendNotification(const OutgoingNotificationData& data); + + virtual void AddSubscribedServiceUrl(const std::string& service_url); + + // MediatorThread::Delegate implementation. + + virtual void OnConnectionStateChange(bool logged_in); + + virtual void OnSubscriptionStateChange(bool subscribed); + + virtual void OnIncomingNotification( + const IncomingNotificationData& notification_data); + + virtual void OnOutgoingNotification(); + + private: + struct TalkMediatorState { + TalkMediatorState() + : started(0), connected(0), initialized(0), logging_in(0), + logged_in(0), subscribed(0) { + } + + unsigned int started : 1; // Background thread has started. + unsigned int connected : 1; // Connected to the mediator thread signal. + unsigned int initialized : 1; // Initialized with login information. + unsigned int logging_in : 1; // Logging in to the mediator's + // authenticator. + unsigned int logged_in : 1; // Logged in the mediator's authenticator. + unsigned int subscribed : 1; // Subscribed to the xmpp receiving channel. + }; + + NonThreadSafe non_thread_safe_; + + // Delegate, which we don't own. May be NULL. + TalkMediator::Delegate* delegate_; + + // Internal state. + TalkMediatorState state_; + + // Cached and verfied from the SetAuthToken method. + buzz::XmppClientSettings xmpp_settings_; + + // The worker thread through which talk events are posted and received. + scoped_ptr<MediatorThread> mediator_thread_; + + const bool invalidate_xmpp_auth_token_; + + std::vector<std::string> subscribed_services_list_; + + FRIEND_TEST(TalkMediatorImplTest, SetAuthTokenWithBadInput); + FRIEND_TEST(TalkMediatorImplTest, SetAuthTokenWithGoodInput); + FRIEND_TEST(TalkMediatorImplTest, SendNotification); + FRIEND_TEST(TalkMediatorImplTest, MediatorThreadCallbacks); + DISALLOW_COPY_AND_ASSIGN(TalkMediatorImpl); +}; + +} // namespace notifier + +#endif // JINGLE_NOTIFIER_LISTENER_TALK_MEDIATOR_IMPL_H_ diff --git a/jingle/notifier/listener/talk_mediator_unittest.cc b/jingle/notifier/listener/talk_mediator_unittest.cc new file mode 100644 index 0000000..88af342 --- /dev/null +++ b/jingle/notifier/listener/talk_mediator_unittest.cc @@ -0,0 +1,207 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <string> + +#include "base/basictypes.h" +#include "base/logging.h" +#include "base/message_loop.h" +#include "jingle/notifier/listener/mediator_thread_mock.h" +#include "jingle/notifier/listener/mediator_thread_impl.h" +#include "jingle/notifier/listener/talk_mediator_impl.h" +#include "talk/xmpp/xmppengine.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace notifier { + +using ::testing::_; + +class MockTalkMediatorDelegate : public TalkMediator::Delegate { + public: + MockTalkMediatorDelegate() {} + virtual ~MockTalkMediatorDelegate() {} + + MOCK_METHOD1(OnNotificationStateChange, + void(bool notification_changed)); + MOCK_METHOD1(OnIncomingNotification, + void(const IncomingNotificationData& data)); + MOCK_METHOD0(OnOutgoingNotification, void()); + + private: + DISALLOW_COPY_AND_ASSIGN(MockTalkMediatorDelegate); +}; + +class TalkMediatorImplTest : public testing::Test { + protected: + TalkMediatorImplTest() {} + virtual ~TalkMediatorImplTest() {} + + TalkMediatorImpl* NewTalkMediator() { + const bool kInitializeSsl = true; + const bool kConnectImmediately = false; + const bool kInvalidateXmppAuthToken = false; + return new TalkMediatorImpl(new MediatorThreadImpl(), kInitializeSsl, + kConnectImmediately, kInvalidateXmppAuthToken); + } + + TalkMediatorImpl* NewMockedTalkMediator( + MockMediatorThread* mock_mediator_thread) { + const bool kInitializeSsl = false; + const bool kConnectImmediately = true; + const bool kInvalidateXmppAuthToken = false; + return new TalkMediatorImpl(mock_mediator_thread, + kInitializeSsl, kConnectImmediately, + kInvalidateXmppAuthToken); + } + + int last_message_; + + private: + // TalkMediatorImpl expects a message loop. + MessageLoop message_loop_; + + DISALLOW_COPY_AND_ASSIGN(TalkMediatorImplTest); +}; + +TEST_F(TalkMediatorImplTest, ConstructionOfTheClass) { + // Constructing a single talk mediator enables SSL through the singleton. + scoped_ptr<TalkMediatorImpl> talk1(NewTalkMediator()); +} + +TEST_F(TalkMediatorImplTest, SetAuthTokenWithBadInput) { + scoped_ptr<TalkMediatorImpl> talk1( + NewMockedTalkMediator(new MockMediatorThread())); + EXPECT_FALSE(talk1->SetAuthToken("@missinguser.com", "", "fake_service")); + EXPECT_FALSE(talk1->state_.initialized); + + scoped_ptr<TalkMediatorImpl> talk2( + NewMockedTalkMediator(new MockMediatorThread())); + EXPECT_FALSE(talk2->SetAuthToken("", "1234567890", "fake_service")); + EXPECT_FALSE(talk2->state_.initialized); + + scoped_ptr<TalkMediatorImpl> talk3( + NewMockedTalkMediator(new MockMediatorThread())); + EXPECT_FALSE(talk3->SetAuthToken("missingdomain", "abcde", "fake_service")); + EXPECT_FALSE(talk3->state_.initialized); +} + +TEST_F(TalkMediatorImplTest, SetAuthTokenWithGoodInput) { + scoped_ptr<TalkMediatorImpl> talk1( + NewMockedTalkMediator(new MockMediatorThread())); + EXPECT_TRUE(talk1->SetAuthToken("chromium@gmail.com", "token", + "fake_service")); + EXPECT_TRUE(talk1->state_.initialized); + + scoped_ptr<TalkMediatorImpl> talk2( + NewMockedTalkMediator(new MockMediatorThread())); + EXPECT_TRUE(talk2->SetAuthToken("chromium@mail.google.com", "token", + "fake_service")); + EXPECT_TRUE(talk2->state_.initialized); + + scoped_ptr<TalkMediatorImpl> talk3( + NewMockedTalkMediator(new MockMediatorThread())); + EXPECT_TRUE(talk3->SetAuthToken("chromium@chromium.org", "token", + "fake_service")); + EXPECT_TRUE(talk3->state_.initialized); +} + +TEST_F(TalkMediatorImplTest, LoginWiring) { + // The TalkMediatorImpl owns the mock. + MockMediatorThread* mock = new MockMediatorThread(); + scoped_ptr<TalkMediatorImpl> talk1(NewMockedTalkMediator(mock)); + + // Login checks states for initialization. + EXPECT_FALSE(talk1->Login()); + EXPECT_EQ(0, mock->login_calls); + + EXPECT_TRUE(talk1->SetAuthToken("chromium@gmail.com", "token", + "fake_service")); + EXPECT_TRUE(talk1->Login()); + EXPECT_EQ(1, mock->login_calls); + + // Successive calls to login will fail. One needs to create a new talk + // mediator object. + EXPECT_FALSE(talk1->Login()); + EXPECT_EQ(1, mock->login_calls); + + EXPECT_TRUE(talk1->Logout()); + EXPECT_EQ(1, mock->logout_calls); + + // Successive logout calls do nothing. + EXPECT_FALSE(talk1->Logout()); + EXPECT_EQ(1, mock->logout_calls); +} + +TEST_F(TalkMediatorImplTest, SendNotification) { + // The TalkMediatorImpl owns the mock. + MockMediatorThread* mock = new MockMediatorThread(); + scoped_ptr<TalkMediatorImpl> talk1(NewMockedTalkMediator(mock)); + + // Failure due to not being logged in. + OutgoingNotificationData data; + EXPECT_FALSE(talk1->SendNotification(data)); + EXPECT_EQ(0, mock->send_calls); + + EXPECT_TRUE(talk1->SetAuthToken("chromium@gmail.com", "token", + "fake_service")); + EXPECT_TRUE(talk1->Login()); + talk1->OnConnectionStateChange(true); + EXPECT_EQ(1, mock->login_calls); + + // Should be subscribed now. + EXPECT_TRUE(talk1->state_.subscribed); + EXPECT_TRUE(talk1->SendNotification(data)); + EXPECT_EQ(1, mock->send_calls); + EXPECT_TRUE(talk1->SendNotification(data)); + EXPECT_EQ(2, mock->send_calls); + + EXPECT_TRUE(talk1->Logout()); + EXPECT_EQ(1, mock->logout_calls); + + // Failure due to being logged out. + EXPECT_FALSE(talk1->SendNotification(data)); + EXPECT_EQ(2, mock->send_calls); +} + +TEST_F(TalkMediatorImplTest, MediatorThreadCallbacks) { + // The TalkMediatorImpl owns the mock. + MockMediatorThread* mock = new MockMediatorThread(); + scoped_ptr<TalkMediatorImpl> talk1(NewMockedTalkMediator(mock)); + + MockTalkMediatorDelegate mock_delegate; + EXPECT_CALL(mock_delegate, OnNotificationStateChange(true)); + EXPECT_CALL(mock_delegate, OnIncomingNotification(_)); + EXPECT_CALL(mock_delegate, OnOutgoingNotification()); + + talk1->SetDelegate(&mock_delegate); + + EXPECT_TRUE(talk1->SetAuthToken("chromium@gmail.com", "token", + "fake_service")); + EXPECT_TRUE(talk1->Login()); + EXPECT_EQ(1, mock->login_calls); + + // The message triggers calls to listen and subscribe. + EXPECT_EQ(1, mock->listen_calls); + EXPECT_EQ(1, mock->subscribe_calls); + EXPECT_TRUE(talk1->state_.subscribed); + + // After subscription success is receieved, the talk mediator will allow + // sending of notifications. + OutgoingNotificationData outgoing_data; + EXPECT_TRUE(talk1->SendNotification(outgoing_data)); + EXPECT_EQ(1, mock->send_calls); + + IncomingNotificationData incoming_data; + incoming_data.service_url = "service_url"; + incoming_data.service_specific_data = "service_data"; + mock->ReceiveNotification(incoming_data); + + // Shouldn't trigger a call to the delegate since we disconnect + // it before we logout. + talk1.reset(); + EXPECT_EQ(1, mock->logout_calls); +} + +} // namespace notifier diff --git a/jingle/notifier/listener/xml_element_util.cc b/jingle/notifier/listener/xml_element_util.cc new file mode 100644 index 0000000..9db99c9 --- /dev/null +++ b/jingle/notifier/listener/xml_element_util.cc @@ -0,0 +1,51 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "jingle/notifier/listener/xml_element_util.h" + +#include <sstream> +#include <string> + +#include "base/string_util.h" +#include "talk/xmllite/qname.h" +#include "talk/xmllite/xmlconstants.h" +#include "talk/xmllite/xmlelement.h" +#include "talk/xmllite/xmlprinter.h" + +namespace notifier { + +std::string XmlElementToString(const buzz::XmlElement& xml_element) { + std::ostringstream xml_stream; + buzz::XmlPrinter::PrintXml(&xml_stream, &xml_element); + return xml_stream.str(); +} + +buzz::XmlElement* MakeBoolXmlElement(const char* name, bool value) { + const buzz::QName elementQName(buzz::STR_EMPTY, name); + const buzz::QName boolAttrQName(buzz::STR_EMPTY, "bool"); + buzz::XmlElement* bool_xml_element = + new buzz::XmlElement(elementQName, true); + bool_xml_element->AddAttr(boolAttrQName, value ? "true" : "false"); + return bool_xml_element; +} + +buzz::XmlElement* MakeIntXmlElement(const char* name, int value) { + const buzz::QName elementQName(buzz::STR_EMPTY, name); + const buzz::QName intAttrQName(buzz::STR_EMPTY, "int"); + buzz::XmlElement* int_xml_element = + new buzz::XmlElement(elementQName, true); + int_xml_element->AddAttr(intAttrQName, IntToString(value)); + return int_xml_element; +} + +buzz::XmlElement* MakeStringXmlElement(const char* name, const char* value) { + const buzz::QName elementQName(buzz::STR_EMPTY, name); + const buzz::QName dataAttrQName(buzz::STR_EMPTY, "data"); + buzz::XmlElement* data_xml_element = + new buzz::XmlElement(elementQName, true); + data_xml_element->AddAttr(dataAttrQName, value); + return data_xml_element; +} + +} // namespace notifier diff --git a/jingle/notifier/listener/xml_element_util.h b/jingle/notifier/listener/xml_element_util.h new file mode 100644 index 0000000..bde8657 --- /dev/null +++ b/jingle/notifier/listener/xml_element_util.h @@ -0,0 +1,29 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef JINGLE_NOTIFIER_LISTENER_XML_ELEMENT_UTIL_H_ +#define JINGLE_NOTIFIER_LISTENER_XML_ELEMENT_UTIL_H_ + +#include <string> + +namespace buzz { +class XmlElement; +} + +namespace notifier { + +std::string XmlElementToString(const buzz::XmlElement& xml_element); + +// The functions below are helpful for building notifications-related +// XML stanzas. + +buzz::XmlElement* MakeBoolXmlElement(const char* name, bool value); + +buzz::XmlElement* MakeIntXmlElement(const char* name, int value); + +buzz::XmlElement* MakeStringXmlElement(const char* name, const char* value); + +} // namespace notifier + +#endif // JINGLE_NOTIFIER_LISTENER_XML_ELEMENT_UTIL_H_ diff --git a/jingle/notifier/listener/xml_element_util_unittest.cc b/jingle/notifier/listener/xml_element_util_unittest.cc new file mode 100644 index 0000000..4566fe8 --- /dev/null +++ b/jingle/notifier/listener/xml_element_util_unittest.cc @@ -0,0 +1,59 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "jingle/notifier/listener/xml_element_util.h" + +#include <sstream> +#include <string> + +#include "base/logging.h" +#include "base/scoped_ptr.h" +#include "talk/xmllite/qname.h" +#include "talk/xmllite/xmlelement.h" +#include "talk/xmllite/xmlprinter.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace buzz { +class XmlElement; +} + +namespace notifier { +namespace { + +class XmlElementUtilTest : public testing::Test {}; + +TEST_F(XmlElementUtilTest, XmlElementToString) { + const buzz::QName kQName("namespace", "element"); + const buzz::XmlElement kXmlElement(kQName, true); + std::ostringstream expected_xml_stream; + buzz::XmlPrinter::PrintXml(&expected_xml_stream, &kXmlElement); + EXPECT_EQ(expected_xml_stream.str(), XmlElementToString(kXmlElement)); +} + +TEST_F(XmlElementUtilTest, MakeBoolXmlElement) { + scoped_ptr<buzz::XmlElement> foo_false( + MakeBoolXmlElement("foo", false)); + EXPECT_EQ("<foo xmlns=\"\" bool=\"false\"/>", XmlElementToString(*foo_false)); + + scoped_ptr<buzz::XmlElement> bar_true( + MakeBoolXmlElement("bar", true)); + EXPECT_EQ("<bar xmlns=\"\" bool=\"true\"/>", XmlElementToString(*bar_true)); +} + +TEST_F(XmlElementUtilTest, MakeIntXmlElement) { + scoped_ptr<buzz::XmlElement> int_xml_element( + MakeIntXmlElement("foo", 35)); + EXPECT_EQ("<foo xmlns=\"\" int=\"35\"/>", + XmlElementToString(*int_xml_element)); +} + +TEST_F(XmlElementUtilTest, MakeStringXmlElement) { + scoped_ptr<buzz::XmlElement> string_xml_element( + MakeStringXmlElement("foo", "bar")); + EXPECT_EQ("<foo xmlns=\"\" data=\"bar\"/>", + XmlElementToString(*string_xml_element)); +} + +} // namespace +} // namespace notifier |