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