cloud_print_proxy_backend.cc revision 0f1bc08d4cfcc34181b0b5cbf065c40f687bf740
1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "chrome/service/cloud_print/cloud_print_proxy_backend.h"
6
7#include <map>
8#include <vector>
9
10#include "base/bind.h"
11#include "base/command_line.h"
12#include "base/compiler_specific.h"
13#include "base/metrics/histogram.h"
14#include "base/rand_util.h"
15#include "base/values.h"
16#include "chrome/common/chrome_switches.h"
17#include "chrome/common/cloud_print/cloud_print_constants.h"
18#include "chrome/service/cloud_print/cloud_print_auth.h"
19#include "chrome/service/cloud_print/cloud_print_connector.h"
20#include "chrome/service/cloud_print/cloud_print_helpers.h"
21#include "chrome/service/cloud_print/cloud_print_token_store.h"
22#include "chrome/service/cloud_print/connector_settings.h"
23#include "chrome/service/net/service_url_request_context.h"
24#include "chrome/service/service_process.h"
25#include "google_apis/gaia/gaia_oauth_client.h"
26#include "google_apis/gaia/gaia_urls.h"
27#include "grit/generated_resources.h"
28#include "jingle/notifier/base/notifier_options.h"
29#include "jingle/notifier/listener/push_client.h"
30#include "jingle/notifier/listener/push_client_observer.h"
31#include "ui/base/l10n/l10n_util.h"
32#include "url/gurl.h"
33
34namespace cloud_print {
35
36// The real guts of CloudPrintProxyBackend, to keep the public client API clean.
37class CloudPrintProxyBackend::Core
38    : public base::RefCountedThreadSafe<CloudPrintProxyBackend::Core>,
39      public CloudPrintAuth::Client,
40      public CloudPrintConnector::Client,
41      public notifier::PushClientObserver {
42 public:
43  // It is OK for print_server_url to be empty. In this case system should
44  // use system default (local) print server.
45  Core(CloudPrintProxyBackend* backend,
46       const ConnectorSettings& settings,
47       const gaia::OAuthClientInfo& oauth_client_info,
48       bool enable_job_poll);
49
50  // Note:
51  //
52  // The Do* methods are the various entry points from CloudPrintProxyBackend
53  // It calls us on a dedicated thread to actually perform synchronous
54  // (and potentially blocking) operations.
55  void DoInitializeWithToken(const std::string& cloud_print_token);
56  void DoInitializeWithRobotToken(const std::string& robot_oauth_refresh_token,
57                                  const std::string& robot_email);
58  void DoInitializeWithRobotAuthCode(const std::string& robot_oauth_auth_code,
59                                     const std::string& robot_email);
60
61  // Called on the CloudPrintProxyBackend core_thread_ to perform
62  // shutdown.
63  void DoShutdown();
64  void DoRegisterSelectedPrinters(
65      const printing::PrinterList& printer_list);
66  void DoUnregisterPrinters();
67
68  // CloudPrintAuth::Client implementation.
69  virtual void OnAuthenticationComplete(
70      const std::string& access_token,
71      const std::string& robot_oauth_refresh_token,
72      const std::string& robot_email,
73      const std::string& user_email) OVERRIDE;
74  virtual void OnInvalidCredentials() OVERRIDE;
75
76  // CloudPrintConnector::Client implementation.
77  virtual void OnAuthFailed() OVERRIDE;
78  virtual void OnXmppPingUpdated(int ping_timeout) OVERRIDE;
79
80  // notifier::PushClientObserver implementation.
81  virtual void OnNotificationsEnabled() OVERRIDE;
82  virtual void OnNotificationsDisabled(
83      notifier::NotificationsDisabledReason reason) OVERRIDE;
84  virtual void OnIncomingNotification(
85      const notifier::Notification& notification) OVERRIDE;
86  virtual void OnPingResponse() OVERRIDE;
87
88 private:
89  friend class base::RefCountedThreadSafe<Core>;
90
91  virtual ~Core() {}
92
93  void CreateAuthAndConnector();
94  void DestroyAuthAndConnector();
95
96  // NotifyXXX is how the Core communicates with the frontend across
97  // threads.
98  void NotifyPrinterListAvailable(
99      const printing::PrinterList& printer_list);
100  void NotifyAuthenticated(
101    const std::string& robot_oauth_refresh_token,
102    const std::string& robot_email,
103    const std::string& user_email);
104  void NotifyAuthenticationFailed();
105  void NotifyPrintSystemUnavailable();
106  void NotifyUnregisterPrinters(const std::string& auth_token,
107                                const std::list<std::string>& printer_ids);
108  void NotifyXmppPingUpdated(int ping_timeout);
109
110  // Init XMPP channel
111  void InitNotifications(const std::string& robot_email,
112                         const std::string& access_token);
113
114  void HandlePrinterNotification(const std::string& notification);
115  void PollForJobs();
116  // Schedules a task to poll for jobs. Does nothing if a task is already
117  // scheduled.
118  void ScheduleJobPoll();
119  void PingXmppServer();
120  void ScheduleXmppPing();
121  void CheckXmppPingStatus();
122
123  CloudPrintTokenStore* GetTokenStore();
124
125  // Our parent CloudPrintProxyBackend
126  CloudPrintProxyBackend* backend_;
127
128  // Cloud Print authenticator.
129  scoped_refptr<CloudPrintAuth> auth_;
130
131  // Cloud Print connector.
132  scoped_refptr<CloudPrintConnector> connector_;
133
134  // OAuth client info.
135  gaia::OAuthClientInfo oauth_client_info_;
136  // Notification (xmpp) handler.
137  scoped_ptr<notifier::PushClient> push_client_;
138  // Indicates whether XMPP notifications are currently enabled.
139  bool notifications_enabled_;
140  // The time when notifications were enabled. Valid only when
141  // notifications_enabled_ is true.
142  base::TimeTicks notifications_enabled_since_;
143  // Indicates whether a task to poll for jobs has been scheduled.
144  bool job_poll_scheduled_;
145  // Indicates whether we should poll for jobs when we lose XMPP connection.
146  bool enable_job_poll_;
147  // Indicates whether a task to ping xmpp server has been scheduled.
148  bool xmpp_ping_scheduled_;
149  // Number of XMPP pings pending reply from the server.
150  int pending_xmpp_pings_;
151  // Connector settings.
152  ConnectorSettings settings_;
153  std::string robot_email_;
154  scoped_ptr<CloudPrintTokenStore> token_store_;
155
156  DISALLOW_COPY_AND_ASSIGN(Core);
157};
158
159CloudPrintProxyBackend::CloudPrintProxyBackend(
160    CloudPrintProxyFrontend* frontend,
161    const ConnectorSettings& settings,
162    const gaia::OAuthClientInfo& oauth_client_info,
163    bool enable_job_poll)
164    : core_thread_("Chrome_CloudPrintProxyCoreThread"),
165      frontend_loop_(base::MessageLoop::current()),
166      frontend_(frontend) {
167  DCHECK(frontend_);
168  core_ = new Core(this, settings, oauth_client_info, enable_job_poll);
169}
170
171CloudPrintProxyBackend::~CloudPrintProxyBackend() { DCHECK(!core_.get()); }
172
173bool CloudPrintProxyBackend::InitializeWithToken(
174    const std::string& cloud_print_token) {
175  if (!core_thread_.Start())
176    return false;
177  core_thread_.message_loop()->PostTask(
178      FROM_HERE,
179      base::Bind(&CloudPrintProxyBackend::Core::DoInitializeWithToken,
180                 core_.get(), cloud_print_token));
181  return true;
182}
183
184bool CloudPrintProxyBackend::InitializeWithRobotToken(
185    const std::string& robot_oauth_refresh_token,
186    const std::string& robot_email) {
187  if (!core_thread_.Start())
188    return false;
189  core_thread_.message_loop()->PostTask(
190      FROM_HERE,
191      base::Bind(&CloudPrintProxyBackend::Core::DoInitializeWithRobotToken,
192                 core_.get(), robot_oauth_refresh_token, robot_email));
193  return true;
194}
195
196bool CloudPrintProxyBackend::InitializeWithRobotAuthCode(
197    const std::string& robot_oauth_auth_code,
198    const std::string& robot_email) {
199  if (!core_thread_.Start())
200    return false;
201  core_thread_.message_loop()->PostTask(
202      FROM_HERE,
203      base::Bind(&CloudPrintProxyBackend::Core::DoInitializeWithRobotAuthCode,
204                 core_.get(), robot_oauth_auth_code, robot_email));
205  return true;
206}
207
208void CloudPrintProxyBackend::Shutdown() {
209  core_thread_.message_loop()->PostTask(
210      FROM_HERE,
211      base::Bind(&CloudPrintProxyBackend::Core::DoShutdown, core_.get()));
212  core_thread_.Stop();
213  core_ = NULL;  // Releases reference to core_.
214}
215
216void CloudPrintProxyBackend::UnregisterPrinters() {
217  core_thread_.message_loop()->PostTask(
218      FROM_HERE,
219      base::Bind(&CloudPrintProxyBackend::Core::DoUnregisterPrinters,
220                 core_.get()));
221}
222
223CloudPrintProxyBackend::Core::Core(
224    CloudPrintProxyBackend* backend,
225    const ConnectorSettings& settings,
226    const gaia::OAuthClientInfo& oauth_client_info,
227    bool enable_job_poll)
228      : backend_(backend),
229        oauth_client_info_(oauth_client_info),
230        notifications_enabled_(false),
231        job_poll_scheduled_(false),
232        enable_job_poll_(enable_job_poll),
233        xmpp_ping_scheduled_(false),
234        pending_xmpp_pings_(0) {
235  settings_.CopyFrom(settings);
236}
237
238void CloudPrintProxyBackend::Core::CreateAuthAndConnector() {
239  if (!auth_.get()) {
240    auth_ = new CloudPrintAuth(this, settings_.server_url(), oauth_client_info_,
241                               settings_.proxy_id());
242  }
243
244  if (!connector_.get()) {
245    connector_ = new CloudPrintConnector(this, settings_);
246  }
247}
248
249void CloudPrintProxyBackend::Core::DestroyAuthAndConnector() {
250  auth_ = NULL;
251  connector_ = NULL;
252}
253
254void CloudPrintProxyBackend::Core::DoInitializeWithToken(
255    const std::string& cloud_print_token) {
256  DCHECK(base::MessageLoop::current() == backend_->core_thread_.message_loop());
257  CreateAuthAndConnector();
258  auth_->AuthenticateWithToken(cloud_print_token);
259}
260
261void CloudPrintProxyBackend::Core::DoInitializeWithRobotToken(
262    const std::string& robot_oauth_refresh_token,
263    const std::string& robot_email) {
264  DCHECK(base::MessageLoop::current() == backend_->core_thread_.message_loop());
265  CreateAuthAndConnector();
266  auth_->AuthenticateWithRobotToken(robot_oauth_refresh_token, robot_email);
267}
268
269void CloudPrintProxyBackend::Core::DoInitializeWithRobotAuthCode(
270    const std::string& robot_oauth_auth_code,
271    const std::string& robot_email) {
272  DCHECK(base::MessageLoop::current() == backend_->core_thread_.message_loop());
273  CreateAuthAndConnector();
274  auth_->AuthenticateWithRobotAuthCode(robot_oauth_auth_code, robot_email);
275}
276
277void CloudPrintProxyBackend::Core::OnAuthenticationComplete(
278    const std::string& access_token,
279    const std::string& robot_oauth_refresh_token,
280    const std::string& robot_email,
281    const std::string& user_email) {
282  CloudPrintTokenStore* token_store  = GetTokenStore();
283  bool first_time = token_store->token().empty();
284  token_store->SetToken(access_token);
285  robot_email_ = robot_email;
286  // Let the frontend know that we have authenticated.
287  backend_->frontend_loop_->PostTask(
288      FROM_HERE,
289      base::Bind(&Core::NotifyAuthenticated, this, robot_oauth_refresh_token,
290                 robot_email, user_email));
291  if (first_time) {
292    InitNotifications(robot_email, access_token);
293  } else {
294    // If we are refreshing a token, update the XMPP token too.
295    DCHECK(push_client_.get());
296    push_client_->UpdateCredentials(robot_email, access_token);
297  }
298  // Start cloud print connector if needed.
299  if (!connector_->IsRunning()) {
300    if (!connector_->Start()) {
301      // Let the frontend know that we do not have a print system.
302      backend_->frontend_loop_->PostTask(
303          FROM_HERE, base::Bind(&Core::NotifyPrintSystemUnavailable, this));
304    }
305  }
306}
307
308void CloudPrintProxyBackend::Core::OnInvalidCredentials() {
309  DCHECK(base::MessageLoop::current() == backend_->core_thread_.message_loop());
310  VLOG(1) << "CP_CONNECTOR: Auth Error";
311  backend_->frontend_loop_->PostTask(
312      FROM_HERE, base::Bind(&Core::NotifyAuthenticationFailed, this));
313}
314
315void CloudPrintProxyBackend::Core::OnAuthFailed() {
316  VLOG(1) << "CP_CONNECTOR: Authentication failed in connector.";
317  // Let's stop connecter and refresh token. We'll restart connecter once
318  // new token available.
319  if (connector_->IsRunning())
320    connector_->Stop();
321
322  // Refresh Auth token.
323  auth_->RefreshAccessToken();
324}
325
326void CloudPrintProxyBackend::Core::OnXmppPingUpdated(int ping_timeout) {
327  settings_.SetXmppPingTimeoutSec(ping_timeout);
328  backend_->frontend_loop_->PostTask(
329      FROM_HERE,
330      base::Bind(&Core::NotifyXmppPingUpdated, this, ping_timeout));
331}
332
333void CloudPrintProxyBackend::Core::InitNotifications(
334    const std::string& robot_email,
335    const std::string& access_token) {
336  DCHECK(base::MessageLoop::current() == backend_->core_thread_.message_loop());
337
338  pending_xmpp_pings_ = 0;
339  notifier::NotifierOptions notifier_options;
340  notifier_options.request_context_getter =
341      g_service_process->GetServiceURLRequestContextGetter();
342  notifier_options.auth_mechanism = "X-OAUTH2";
343  notifier_options.try_ssltcp_first = true;
344  push_client_ = notifier::PushClient::CreateDefault(notifier_options);
345  push_client_->AddObserver(this);
346  notifier::Subscription subscription;
347  subscription.channel = kCloudPrintPushNotificationsSource;
348  subscription.from = kCloudPrintPushNotificationsSource;
349  push_client_->UpdateSubscriptions(
350      notifier::SubscriptionList(1, subscription));
351  push_client_->UpdateCredentials(robot_email, access_token);
352}
353
354void CloudPrintProxyBackend::Core::DoShutdown() {
355  DCHECK(base::MessageLoop::current() == backend_->core_thread_.message_loop());
356  VLOG(1) << "CP_CONNECTOR: Shutdown connector, id: " << settings_.proxy_id();
357
358  if (connector_->IsRunning())
359    connector_->Stop();
360
361  // Important to delete the PushClient on this thread.
362  if (push_client_.get()) {
363    push_client_->RemoveObserver(this);
364  }
365  push_client_.reset();
366  notifications_enabled_ = false;
367  notifications_enabled_since_ = base::TimeTicks();
368  token_store_.reset();
369
370  DestroyAuthAndConnector();
371}
372
373void CloudPrintProxyBackend::Core::DoUnregisterPrinters() {
374  DCHECK(base::MessageLoop::current() == backend_->core_thread_.message_loop());
375
376  std::string access_token = GetTokenStore()->token();
377
378  std::list<std::string> printer_ids;
379  connector_->GetPrinterIds(&printer_ids);
380
381  backend_->frontend_loop_->PostTask(
382      FROM_HERE,
383      base::Bind(&Core::NotifyUnregisterPrinters,
384                 this, access_token, printer_ids));
385}
386
387void CloudPrintProxyBackend::Core::HandlePrinterNotification(
388    const std::string& notification) {
389  DCHECK(base::MessageLoop::current() == backend_->core_thread_.message_loop());
390
391  size_t pos = notification.rfind(kNotificationUpdateSettings);
392  if (pos == std::string::npos) {
393    VLOG(1) << "CP_CONNECTOR: Handle printer notification, id: "
394            << notification;
395    connector_->CheckForJobs(kJobFetchReasonNotified, notification);
396  } else {
397    DCHECK(pos == notification.length() - strlen(kNotificationUpdateSettings));
398    std::string printer_id = notification.substr(0, pos);
399    VLOG(1) << "CP_CONNECTOR: Update printer settings, id: " << printer_id;
400    connector_->UpdatePrinterSettings(printer_id);
401  }
402}
403
404void CloudPrintProxyBackend::Core::PollForJobs() {
405  VLOG(1) << "CP_CONNECTOR: Polling for jobs.";
406  DCHECK(base::MessageLoop::current() == backend_->core_thread_.message_loop());
407  // Check all printers for jobs.
408  connector_->CheckForJobs(kJobFetchReasonPoll, std::string());
409
410  job_poll_scheduled_ = false;
411  // If we don't have notifications and job polling is enabled, poll again
412  // after a while.
413  if (!notifications_enabled_ && enable_job_poll_)
414    ScheduleJobPoll();
415}
416
417void CloudPrintProxyBackend::Core::ScheduleJobPoll() {
418  if (!job_poll_scheduled_) {
419    base::TimeDelta interval = base::TimeDelta::FromSeconds(
420        base::RandInt(kMinJobPollIntervalSecs, kMaxJobPollIntervalSecs));
421    base::MessageLoop::current()->PostDelayedTask(
422        FROM_HERE,
423        base::Bind(&CloudPrintProxyBackend::Core::PollForJobs, this),
424        interval);
425    job_poll_scheduled_ = true;
426  }
427}
428
429void CloudPrintProxyBackend::Core::PingXmppServer() {
430  xmpp_ping_scheduled_ = false;
431
432  if (!push_client_.get())
433    return;
434
435  push_client_->SendPing();
436
437  pending_xmpp_pings_++;
438  if (pending_xmpp_pings_ >= kMaxFailedXmppPings) {
439    // Check ping status when we close to the limit.
440    base::MessageLoop::current()->PostDelayedTask(
441        FROM_HERE,
442        base::Bind(&CloudPrintProxyBackend::Core::CheckXmppPingStatus, this),
443        base::TimeDelta::FromSeconds(kXmppPingCheckIntervalSecs));
444  }
445
446  // Schedule next ping if needed.
447  if (notifications_enabled_)
448    ScheduleXmppPing();
449}
450
451void CloudPrintProxyBackend::Core::ScheduleXmppPing() {
452  // settings_.xmpp_ping_enabled() is obsolete, we are now control
453  // XMPP pings from Cloud Print server.
454  if (!xmpp_ping_scheduled_) {
455    base::TimeDelta interval = base::TimeDelta::FromSeconds(
456      base::RandInt(settings_.xmpp_ping_timeout_sec() * 0.9,
457                    settings_.xmpp_ping_timeout_sec() * 1.1));
458    base::MessageLoop::current()->PostDelayedTask(
459        FROM_HERE,
460        base::Bind(&CloudPrintProxyBackend::Core::PingXmppServer, this),
461        interval);
462    xmpp_ping_scheduled_ = true;
463  }
464}
465
466void CloudPrintProxyBackend::Core::CheckXmppPingStatus() {
467  if (pending_xmpp_pings_ >= kMaxFailedXmppPings) {
468    UMA_HISTOGRAM_COUNTS_100("CloudPrint.XmppPingTry", 99);  // Max on fail.
469    // Reconnect to XMPP.
470    pending_xmpp_pings_ = 0;
471    push_client_.reset();
472    InitNotifications(robot_email_, GetTokenStore()->token());
473  }
474}
475
476CloudPrintTokenStore* CloudPrintProxyBackend::Core::GetTokenStore() {
477  DCHECK(base::MessageLoop::current() == backend_->core_thread_.message_loop());
478  if (!token_store_.get())
479    token_store_.reset(new CloudPrintTokenStore);
480  return token_store_.get();
481}
482
483void CloudPrintProxyBackend::Core::NotifyAuthenticated(
484    const std::string& robot_oauth_refresh_token,
485    const std::string& robot_email,
486    const std::string& user_email) {
487  DCHECK(base::MessageLoop::current() == backend_->frontend_loop_);
488  backend_->frontend_
489      ->OnAuthenticated(robot_oauth_refresh_token, robot_email, user_email);
490}
491
492void CloudPrintProxyBackend::Core::NotifyAuthenticationFailed() {
493  DCHECK(base::MessageLoop::current() == backend_->frontend_loop_);
494  backend_->frontend_->OnAuthenticationFailed();
495}
496
497void CloudPrintProxyBackend::Core::NotifyPrintSystemUnavailable() {
498  DCHECK(base::MessageLoop::current() == backend_->frontend_loop_);
499  backend_->frontend_->OnPrintSystemUnavailable();
500}
501
502void CloudPrintProxyBackend::Core::NotifyUnregisterPrinters(
503    const std::string& auth_token,
504    const std::list<std::string>& printer_ids) {
505  DCHECK(base::MessageLoop::current() == backend_->frontend_loop_);
506  backend_->frontend_->OnUnregisterPrinters(auth_token, printer_ids);
507}
508
509void CloudPrintProxyBackend::Core::NotifyXmppPingUpdated(int ping_timeout) {
510  DCHECK(base::MessageLoop::current() == backend_->frontend_loop_);
511  backend_->frontend_->OnXmppPingUpdated(ping_timeout);
512}
513
514void CloudPrintProxyBackend::Core::OnNotificationsEnabled() {
515  DCHECK(base::MessageLoop::current() == backend_->core_thread_.message_loop());
516  notifications_enabled_ = true;
517  notifications_enabled_since_ = base::TimeTicks::Now();
518  VLOG(1) << "Notifications for connector " << settings_.proxy_id()
519          << " were enabled at "
520          << notifications_enabled_since_.ToInternalValue();
521  // Notifications just got re-enabled. In this case we want to schedule
522  // a poll once for jobs we might have missed when we were dark.
523  // Note that ScheduleJobPoll will not schedule again if a job poll task is
524  // already scheduled.
525  ScheduleJobPoll();
526
527  // Schedule periodic ping for XMPP notification channel.
528  ScheduleXmppPing();
529}
530
531void CloudPrintProxyBackend::Core::OnNotificationsDisabled(
532    notifier::NotificationsDisabledReason reason) {
533  DCHECK(base::MessageLoop::current() == backend_->core_thread_.message_loop());
534  notifications_enabled_ = false;
535  LOG(ERROR) << "Notifications for connector " << settings_.proxy_id()
536             << " disabled.";
537  notifications_enabled_since_ = base::TimeTicks();
538  // We just lost notifications. This this case we want to schedule a
539  // job poll if enable_job_poll_ is true.
540  if (enable_job_poll_)
541    ScheduleJobPoll();
542}
543
544
545void CloudPrintProxyBackend::Core::OnIncomingNotification(
546    const notifier::Notification& notification) {
547  // Since we got some notification from the server,
548  // reset pending ping counter to 0.
549  pending_xmpp_pings_ = 0;
550
551  DCHECK(base::MessageLoop::current() == backend_->core_thread_.message_loop());
552  VLOG(1) << "CP_CONNECTOR: Incoming notification.";
553  if (0 == base::strcasecmp(kCloudPrintPushNotificationsSource,
554                            notification.channel.c_str()))
555    HandlePrinterNotification(notification.data);
556}
557
558void CloudPrintProxyBackend::Core::OnPingResponse() {
559  UMA_HISTOGRAM_COUNTS_100("CloudPrint.XmppPingTry", pending_xmpp_pings_);
560  pending_xmpp_pings_ = 0;
561  VLOG(1) << "CP_CONNECTOR: Ping response received.";
562}
563
564}  // namespace cloud_print
565