| // Copyright (c) 2012 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 "chrome/service/cloud_print/cloud_print_proxy_backend.h" |
| |
| #include <stddef.h> |
| |
| #include <map> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/command_line.h" |
| #include "base/compiler_specific.h" |
| #include "base/location.h" |
| #include "base/macros.h" |
| #include "base/metrics/histogram.h" |
| #include "base/rand_util.h" |
| #include "base/single_thread_task_runner.h" |
| #include "base/strings/string_util.h" |
| #include "base/thread_task_runner_handle.h" |
| #include "base/values.h" |
| #include "chrome/common/cloud_print/cloud_print_constants.h" |
| #include "chrome/service/cloud_print/cloud_print_auth.h" |
| #include "chrome/service/cloud_print/cloud_print_connector.h" |
| #include "chrome/service/cloud_print/cloud_print_service_helpers.h" |
| #include "chrome/service/cloud_print/cloud_print_token_store.h" |
| #include "chrome/service/cloud_print/connector_settings.h" |
| #include "chrome/service/net/service_url_request_context_getter.h" |
| #include "chrome/service/service_process.h" |
| #include "components/cloud_devices/common/cloud_devices_switches.h" |
| #include "google_apis/gaia/gaia_oauth_client.h" |
| #include "google_apis/gaia/gaia_urls.h" |
| #include "jingle/notifier/base/notifier_options.h" |
| #include "jingle/notifier/listener/push_client.h" |
| #include "jingle/notifier/listener/push_client_observer.h" |
| #include "url/gurl.h" |
| |
| namespace cloud_print { |
| |
| // The real guts of CloudPrintProxyBackend, to keep the public client API clean. |
| class CloudPrintProxyBackend::Core |
| : public base::RefCountedThreadSafe<CloudPrintProxyBackend::Core>, |
| public CloudPrintAuth::Client, |
| public CloudPrintConnector::Client, |
| public notifier::PushClientObserver { |
| public: |
| // It is OK for print_server_url to be empty. In this case system should |
| // use system default (local) print server. |
| Core(CloudPrintProxyBackend* backend, |
| const ConnectorSettings& settings, |
| const gaia::OAuthClientInfo& oauth_client_info, |
| bool enable_job_poll); |
| |
| // Note: |
| // |
| // The Do* methods are the various entry points from CloudPrintProxyBackend |
| // It calls us on a dedicated thread to actually perform synchronous |
| // (and potentially blocking) operations. |
| void DoInitializeWithToken(const std::string& cloud_print_token); |
| void DoInitializeWithRobotToken(const std::string& robot_oauth_refresh_token, |
| const std::string& robot_email); |
| void DoInitializeWithRobotAuthCode(const std::string& robot_oauth_auth_code, |
| const std::string& robot_email); |
| |
| // Called on the CloudPrintProxyBackend core_thread_ to perform |
| // shutdown. |
| void DoShutdown(); |
| void DoRegisterSelectedPrinters( |
| const printing::PrinterList& printer_list); |
| void DoUnregisterPrinters(); |
| |
| // CloudPrintAuth::Client implementation. |
| void OnAuthenticationComplete(const std::string& access_token, |
| const std::string& robot_oauth_refresh_token, |
| const std::string& robot_email, |
| const std::string& user_email) override; |
| void OnInvalidCredentials() override; |
| |
| // CloudPrintConnector::Client implementation. |
| void OnAuthFailed() override; |
| void OnXmppPingUpdated(int ping_timeout) override; |
| |
| // notifier::PushClientObserver implementation. |
| void OnNotificationsEnabled() override; |
| void OnNotificationsDisabled( |
| notifier::NotificationsDisabledReason reason) override; |
| void OnIncomingNotification( |
| const notifier::Notification& notification) override; |
| void OnPingResponse() override; |
| |
| private: |
| friend class base::RefCountedThreadSafe<Core>; |
| |
| ~Core() override {} |
| |
| void CreateAuthAndConnector(); |
| void DestroyAuthAndConnector(); |
| |
| // NotifyXXX is how the Core communicates with the frontend across |
| // threads. |
| void NotifyPrinterListAvailable( |
| const printing::PrinterList& printer_list); |
| void NotifyAuthenticated( |
| const std::string& robot_oauth_refresh_token, |
| const std::string& robot_email, |
| const std::string& user_email); |
| void NotifyAuthenticationFailed(); |
| void NotifyPrintSystemUnavailable(); |
| void NotifyUnregisterPrinters(const std::string& auth_token, |
| const std::list<std::string>& printer_ids); |
| void NotifyXmppPingUpdated(int ping_timeout); |
| |
| // Init XMPP channel |
| void InitNotifications(const std::string& robot_email, |
| const std::string& access_token); |
| |
| void HandlePrinterNotification(const std::string& notification); |
| void PollForJobs(); |
| // Schedules a task to poll for jobs. Does nothing if a task is already |
| // scheduled. |
| void ScheduleJobPoll(); |
| void PingXmppServer(); |
| void ScheduleXmppPing(); |
| void CheckXmppPingStatus(); |
| |
| CloudPrintTokenStore* GetTokenStore(); |
| |
| // Our parent CloudPrintProxyBackend |
| CloudPrintProxyBackend* backend_; |
| |
| // Cloud Print authenticator. |
| scoped_refptr<CloudPrintAuth> auth_; |
| |
| // Cloud Print connector. |
| scoped_refptr<CloudPrintConnector> connector_; |
| |
| // OAuth client info. |
| gaia::OAuthClientInfo oauth_client_info_; |
| // Notification (xmpp) handler. |
| scoped_ptr<notifier::PushClient> push_client_; |
| // Indicates whether XMPP notifications are currently enabled. |
| bool notifications_enabled_; |
| // The time when notifications were enabled. Valid only when |
| // notifications_enabled_ is true. |
| base::TimeTicks notifications_enabled_since_; |
| // Indicates whether a task to poll for jobs has been scheduled. |
| bool job_poll_scheduled_; |
| // Indicates whether we should poll for jobs when we lose XMPP connection. |
| bool enable_job_poll_; |
| // Indicates whether a task to ping xmpp server has been scheduled. |
| bool xmpp_ping_scheduled_; |
| // Number of XMPP pings pending reply from the server. |
| int pending_xmpp_pings_; |
| // Connector settings. |
| ConnectorSettings settings_; |
| std::string robot_email_; |
| scoped_ptr<CloudPrintTokenStore> token_store_; |
| |
| DISALLOW_COPY_AND_ASSIGN(Core); |
| }; |
| |
| CloudPrintProxyBackend::CloudPrintProxyBackend( |
| CloudPrintProxyFrontend* frontend, |
| const ConnectorSettings& settings, |
| const gaia::OAuthClientInfo& oauth_client_info, |
| bool enable_job_poll) |
| : core_thread_("Chrome_CloudPrintProxyCoreThread"), |
| frontend_loop_(base::MessageLoop::current()), |
| frontend_(frontend) { |
| DCHECK(frontend_); |
| core_ = new Core(this, settings, oauth_client_info, enable_job_poll); |
| } |
| |
| CloudPrintProxyBackend::~CloudPrintProxyBackend() { DCHECK(!core_.get()); } |
| |
| bool CloudPrintProxyBackend::InitializeWithToken( |
| const std::string& cloud_print_token) { |
| if (!core_thread_.Start()) |
| return false; |
| core_thread_.task_runner()->PostTask( |
| FROM_HERE, |
| base::Bind(&CloudPrintProxyBackend::Core::DoInitializeWithToken, |
| core_.get(), cloud_print_token)); |
| return true; |
| } |
| |
| bool CloudPrintProxyBackend::InitializeWithRobotToken( |
| const std::string& robot_oauth_refresh_token, |
| const std::string& robot_email) { |
| if (!core_thread_.Start()) |
| return false; |
| core_thread_.task_runner()->PostTask( |
| FROM_HERE, |
| base::Bind(&CloudPrintProxyBackend::Core::DoInitializeWithRobotToken, |
| core_.get(), robot_oauth_refresh_token, robot_email)); |
| return true; |
| } |
| |
| bool CloudPrintProxyBackend::InitializeWithRobotAuthCode( |
| const std::string& robot_oauth_auth_code, |
| const std::string& robot_email) { |
| if (!core_thread_.Start()) |
| return false; |
| core_thread_.task_runner()->PostTask( |
| FROM_HERE, |
| base::Bind(&CloudPrintProxyBackend::Core::DoInitializeWithRobotAuthCode, |
| core_.get(), robot_oauth_auth_code, robot_email)); |
| return true; |
| } |
| |
| void CloudPrintProxyBackend::Shutdown() { |
| core_thread_.task_runner()->PostTask( |
| FROM_HERE, |
| base::Bind(&CloudPrintProxyBackend::Core::DoShutdown, core_.get())); |
| core_thread_.Stop(); |
| core_ = NULL; // Releases reference to core_. |
| } |
| |
| void CloudPrintProxyBackend::UnregisterPrinters() { |
| core_thread_.task_runner()->PostTask( |
| FROM_HERE, base::Bind(&CloudPrintProxyBackend::Core::DoUnregisterPrinters, |
| core_.get())); |
| } |
| |
| CloudPrintProxyBackend::Core::Core( |
| CloudPrintProxyBackend* backend, |
| const ConnectorSettings& settings, |
| const gaia::OAuthClientInfo& oauth_client_info, |
| bool enable_job_poll) |
| : backend_(backend), |
| oauth_client_info_(oauth_client_info), |
| notifications_enabled_(false), |
| job_poll_scheduled_(false), |
| enable_job_poll_(enable_job_poll), |
| xmpp_ping_scheduled_(false), |
| pending_xmpp_pings_(0) { |
| settings_.CopyFrom(settings); |
| } |
| |
| void CloudPrintProxyBackend::Core::CreateAuthAndConnector() { |
| if (!auth_.get()) { |
| auth_ = new CloudPrintAuth(this, settings_.server_url(), oauth_client_info_, |
| settings_.proxy_id()); |
| } |
| |
| if (!connector_.get()) { |
| connector_ = new CloudPrintConnector(this, settings_); |
| } |
| } |
| |
| void CloudPrintProxyBackend::Core::DestroyAuthAndConnector() { |
| auth_ = NULL; |
| connector_ = NULL; |
| } |
| |
| void CloudPrintProxyBackend::Core::DoInitializeWithToken( |
| const std::string& cloud_print_token) { |
| DCHECK(base::MessageLoop::current() == backend_->core_thread_.message_loop()); |
| CreateAuthAndConnector(); |
| auth_->AuthenticateWithToken(cloud_print_token); |
| } |
| |
| void CloudPrintProxyBackend::Core::DoInitializeWithRobotToken( |
| const std::string& robot_oauth_refresh_token, |
| const std::string& robot_email) { |
| DCHECK(base::MessageLoop::current() == backend_->core_thread_.message_loop()); |
| CreateAuthAndConnector(); |
| auth_->AuthenticateWithRobotToken(robot_oauth_refresh_token, robot_email); |
| } |
| |
| void CloudPrintProxyBackend::Core::DoInitializeWithRobotAuthCode( |
| const std::string& robot_oauth_auth_code, |
| const std::string& robot_email) { |
| DCHECK(base::MessageLoop::current() == backend_->core_thread_.message_loop()); |
| CreateAuthAndConnector(); |
| auth_->AuthenticateWithRobotAuthCode(robot_oauth_auth_code, robot_email); |
| } |
| |
| void CloudPrintProxyBackend::Core::OnAuthenticationComplete( |
| const std::string& access_token, |
| const std::string& robot_oauth_refresh_token, |
| const std::string& robot_email, |
| const std::string& user_email) { |
| CloudPrintTokenStore* token_store = GetTokenStore(); |
| bool first_time = token_store->token().empty(); |
| token_store->SetToken(access_token); |
| robot_email_ = robot_email; |
| // Let the frontend know that we have authenticated. |
| backend_->frontend_loop_->task_runner()->PostTask( |
| FROM_HERE, |
| base::Bind(&Core::NotifyAuthenticated, this, robot_oauth_refresh_token, |
| robot_email, user_email)); |
| if (first_time) { |
| InitNotifications(robot_email, access_token); |
| } else { |
| // If we are refreshing a token, update the XMPP token too. |
| DCHECK(push_client_.get()); |
| push_client_->UpdateCredentials(robot_email, access_token); |
| } |
| // Start cloud print connector if needed. |
| if (!connector_->IsRunning()) { |
| if (!connector_->Start()) { |
| // Let the frontend know that we do not have a print system. |
| backend_->frontend_loop_->task_runner()->PostTask( |
| FROM_HERE, base::Bind(&Core::NotifyPrintSystemUnavailable, this)); |
| } |
| } |
| } |
| |
| void CloudPrintProxyBackend::Core::OnInvalidCredentials() { |
| DCHECK(base::MessageLoop::current() == backend_->core_thread_.message_loop()); |
| VLOG(1) << "CP_CONNECTOR: Auth Error"; |
| backend_->frontend_loop_->task_runner()->PostTask( |
| FROM_HERE, base::Bind(&Core::NotifyAuthenticationFailed, this)); |
| } |
| |
| void CloudPrintProxyBackend::Core::OnAuthFailed() { |
| VLOG(1) << "CP_CONNECTOR: Authentication failed in connector."; |
| // Let's stop connecter and refresh token. We'll restart connecter once |
| // new token available. |
| if (connector_->IsRunning()) |
| connector_->Stop(); |
| |
| // Refresh Auth token. |
| auth_->RefreshAccessToken(); |
| } |
| |
| void CloudPrintProxyBackend::Core::OnXmppPingUpdated(int ping_timeout) { |
| settings_.SetXmppPingTimeoutSec(ping_timeout); |
| backend_->frontend_loop_->task_runner()->PostTask( |
| FROM_HERE, base::Bind(&Core::NotifyXmppPingUpdated, this, ping_timeout)); |
| } |
| |
| void CloudPrintProxyBackend::Core::InitNotifications( |
| const std::string& robot_email, |
| const std::string& access_token) { |
| DCHECK(base::MessageLoop::current() == backend_->core_thread_.message_loop()); |
| |
| pending_xmpp_pings_ = 0; |
| notifier::NotifierOptions notifier_options; |
| notifier_options.request_context_getter = |
| g_service_process->GetServiceURLRequestContextGetter(); |
| notifier_options.auth_mechanism = "X-OAUTH2"; |
| notifier_options.try_ssltcp_first = true; |
| notifier_options.xmpp_host_port = net::HostPortPair::FromString( |
| base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( |
| switches::kCloudPrintXmppEndpoint)); |
| push_client_ = notifier::PushClient::CreateDefault(notifier_options); |
| push_client_->AddObserver(this); |
| notifier::Subscription subscription; |
| subscription.channel = kCloudPrintPushNotificationsSource; |
| subscription.from = kCloudPrintPushNotificationsSource; |
| push_client_->UpdateSubscriptions( |
| notifier::SubscriptionList(1, subscription)); |
| push_client_->UpdateCredentials(robot_email, access_token); |
| } |
| |
| void CloudPrintProxyBackend::Core::DoShutdown() { |
| DCHECK(base::MessageLoop::current() == backend_->core_thread_.message_loop()); |
| VLOG(1) << "CP_CONNECTOR: Shutdown connector, id: " << settings_.proxy_id(); |
| |
| if (connector_->IsRunning()) |
| connector_->Stop(); |
| |
| // Important to delete the PushClient on this thread. |
| if (push_client_.get()) { |
| push_client_->RemoveObserver(this); |
| } |
| push_client_.reset(); |
| notifications_enabled_ = false; |
| notifications_enabled_since_ = base::TimeTicks(); |
| token_store_.reset(); |
| |
| DestroyAuthAndConnector(); |
| } |
| |
| void CloudPrintProxyBackend::Core::DoUnregisterPrinters() { |
| DCHECK(base::MessageLoop::current() == backend_->core_thread_.message_loop()); |
| |
| std::string access_token = GetTokenStore()->token(); |
| |
| std::list<std::string> printer_ids; |
| connector_->GetPrinterIds(&printer_ids); |
| |
| backend_->frontend_loop_->task_runner()->PostTask( |
| FROM_HERE, base::Bind(&Core::NotifyUnregisterPrinters, this, access_token, |
| printer_ids)); |
| } |
| |
| void CloudPrintProxyBackend::Core::HandlePrinterNotification( |
| const std::string& notification) { |
| DCHECK(base::MessageLoop::current() == backend_->core_thread_.message_loop()); |
| |
| size_t pos = notification.rfind(kNotificationUpdateSettings); |
| if (pos == std::string::npos) { |
| VLOG(1) << "CP_CONNECTOR: Handle printer notification, id: " |
| << notification; |
| connector_->CheckForJobs(kJobFetchReasonNotified, notification); |
| } else { |
| DCHECK(pos == notification.length() - strlen(kNotificationUpdateSettings)); |
| std::string printer_id = notification.substr(0, pos); |
| VLOG(1) << "CP_CONNECTOR: Update printer settings, id: " << printer_id; |
| connector_->UpdatePrinterSettings(printer_id); |
| } |
| } |
| |
| void CloudPrintProxyBackend::Core::PollForJobs() { |
| VLOG(1) << "CP_CONNECTOR: Polling for jobs."; |
| DCHECK(base::MessageLoop::current() == backend_->core_thread_.message_loop()); |
| // Check all printers for jobs. |
| connector_->CheckForJobs(kJobFetchReasonPoll, std::string()); |
| |
| job_poll_scheduled_ = false; |
| // If we don't have notifications and job polling is enabled, poll again |
| // after a while. |
| if (!notifications_enabled_ && enable_job_poll_) |
| ScheduleJobPoll(); |
| } |
| |
| void CloudPrintProxyBackend::Core::ScheduleJobPoll() { |
| if (!job_poll_scheduled_) { |
| base::TimeDelta interval = base::TimeDelta::FromSeconds( |
| base::RandInt(kMinJobPollIntervalSecs, kMaxJobPollIntervalSecs)); |
| base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( |
| FROM_HERE, base::Bind(&CloudPrintProxyBackend::Core::PollForJobs, this), |
| interval); |
| job_poll_scheduled_ = true; |
| } |
| } |
| |
| void CloudPrintProxyBackend::Core::PingXmppServer() { |
| xmpp_ping_scheduled_ = false; |
| |
| if (!push_client_.get()) |
| return; |
| |
| push_client_->SendPing(); |
| |
| pending_xmpp_pings_++; |
| if (pending_xmpp_pings_ >= kMaxFailedXmppPings) { |
| // Check ping status when we close to the limit. |
| base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( |
| FROM_HERE, |
| base::Bind(&CloudPrintProxyBackend::Core::CheckXmppPingStatus, this), |
| base::TimeDelta::FromSeconds(kXmppPingCheckIntervalSecs)); |
| } |
| |
| // Schedule next ping if needed. |
| if (notifications_enabled_) |
| ScheduleXmppPing(); |
| } |
| |
| void CloudPrintProxyBackend::Core::ScheduleXmppPing() { |
| // settings_.xmpp_ping_enabled() is obsolete, we are now control |
| // XMPP pings from Cloud Print server. |
| if (!xmpp_ping_scheduled_) { |
| base::TimeDelta interval = base::TimeDelta::FromSeconds( |
| base::RandInt(settings_.xmpp_ping_timeout_sec() * 0.9, |
| settings_.xmpp_ping_timeout_sec() * 1.1)); |
| base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( |
| FROM_HERE, |
| base::Bind(&CloudPrintProxyBackend::Core::PingXmppServer, this), |
| interval); |
| xmpp_ping_scheduled_ = true; |
| } |
| } |
| |
| void CloudPrintProxyBackend::Core::CheckXmppPingStatus() { |
| if (pending_xmpp_pings_ >= kMaxFailedXmppPings) { |
| UMA_HISTOGRAM_COUNTS_100("CloudPrint.XmppPingTry", 99); // Max on fail. |
| // Reconnect to XMPP. |
| pending_xmpp_pings_ = 0; |
| push_client_.reset(); |
| InitNotifications(robot_email_, GetTokenStore()->token()); |
| } |
| } |
| |
| CloudPrintTokenStore* CloudPrintProxyBackend::Core::GetTokenStore() { |
| DCHECK(base::MessageLoop::current() == backend_->core_thread_.message_loop()); |
| if (!token_store_.get()) |
| token_store_.reset(new CloudPrintTokenStore); |
| return token_store_.get(); |
| } |
| |
| void CloudPrintProxyBackend::Core::NotifyAuthenticated( |
| const std::string& robot_oauth_refresh_token, |
| const std::string& robot_email, |
| const std::string& user_email) { |
| DCHECK(base::MessageLoop::current() == backend_->frontend_loop_); |
| backend_->frontend_ |
| ->OnAuthenticated(robot_oauth_refresh_token, robot_email, user_email); |
| } |
| |
| void CloudPrintProxyBackend::Core::NotifyAuthenticationFailed() { |
| DCHECK(base::MessageLoop::current() == backend_->frontend_loop_); |
| backend_->frontend_->OnAuthenticationFailed(); |
| } |
| |
| void CloudPrintProxyBackend::Core::NotifyPrintSystemUnavailable() { |
| DCHECK(base::MessageLoop::current() == backend_->frontend_loop_); |
| backend_->frontend_->OnPrintSystemUnavailable(); |
| } |
| |
| void CloudPrintProxyBackend::Core::NotifyUnregisterPrinters( |
| const std::string& auth_token, |
| const std::list<std::string>& printer_ids) { |
| DCHECK(base::MessageLoop::current() == backend_->frontend_loop_); |
| backend_->frontend_->OnUnregisterPrinters(auth_token, printer_ids); |
| } |
| |
| void CloudPrintProxyBackend::Core::NotifyXmppPingUpdated(int ping_timeout) { |
| DCHECK(base::MessageLoop::current() == backend_->frontend_loop_); |
| backend_->frontend_->OnXmppPingUpdated(ping_timeout); |
| } |
| |
| void CloudPrintProxyBackend::Core::OnNotificationsEnabled() { |
| DCHECK(base::MessageLoop::current() == backend_->core_thread_.message_loop()); |
| notifications_enabled_ = true; |
| notifications_enabled_since_ = base::TimeTicks::Now(); |
| VLOG(1) << "Notifications for connector " << settings_.proxy_id() |
| << " were enabled at " |
| << notifications_enabled_since_.ToInternalValue(); |
| // Notifications just got re-enabled. In this case we want to schedule |
| // a poll once for jobs we might have missed when we were dark. |
| // Note that ScheduleJobPoll will not schedule again if a job poll task is |
| // already scheduled. |
| ScheduleJobPoll(); |
| |
| // Schedule periodic ping for XMPP notification channel. |
| ScheduleXmppPing(); |
| } |
| |
| void CloudPrintProxyBackend::Core::OnNotificationsDisabled( |
| notifier::NotificationsDisabledReason reason) { |
| DCHECK(base::MessageLoop::current() == backend_->core_thread_.message_loop()); |
| notifications_enabled_ = false; |
| LOG(ERROR) << "Notifications for connector " << settings_.proxy_id() |
| << " disabled."; |
| notifications_enabled_since_ = base::TimeTicks(); |
| // We just lost notifications. This this case we want to schedule a |
| // job poll if enable_job_poll_ is true. |
| if (enable_job_poll_) |
| ScheduleJobPoll(); |
| } |
| |
| |
| void CloudPrintProxyBackend::Core::OnIncomingNotification( |
| const notifier::Notification& notification) { |
| // Since we got some notification from the server, |
| // reset pending ping counter to 0. |
| pending_xmpp_pings_ = 0; |
| |
| DCHECK(base::MessageLoop::current() == backend_->core_thread_.message_loop()); |
| VLOG(1) << "CP_CONNECTOR: Incoming notification."; |
| if (base::EqualsCaseInsensitiveASCII(kCloudPrintPushNotificationsSource, |
| notification.channel)) |
| HandlePrinterNotification(notification.data); |
| } |
| |
| void CloudPrintProxyBackend::Core::OnPingResponse() { |
| UMA_HISTOGRAM_COUNTS_100("CloudPrint.XmppPingTry", pending_xmpp_pings_); |
| pending_xmpp_pings_ = 0; |
| VLOG(1) << "CP_CONNECTOR: Ping response received."; |
| } |
| |
| } // namespace cloud_print |