blob: f8b4da129b9a1e91a357b4c4608fca5a284f286b [file] [log] [blame]
// 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_macros.h"
#include "base/rand_util.h"
#include "base/strings/string_util.h"
#include "base/threading/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 "net/traffic_annotation/network_traffic_annotation.h"
#include "url/gurl.h"
namespace cloud_print {
namespace {
constexpr net::NetworkTrafficAnnotationTag
kCloudPrintCredentialUpdateTrafficAnnotation =
net::DefineNetworkTrafficAnnotation("cloud_print_credential_update",
R"(
semantics {
sender: "Cloud Print Proxy Backend"
description:
"Refreshes the access token for fetching print jobs for Cloud Print."
trigger:
"Aging of the current access token, current value is 5 minutes."
data:
"OAuth2 refresh token."
destination: GOOGLE_OWNED_SERVICE
}
policy {
cookies_allowed: NO
setting: "This feature cannot be disabled by settings."
chrome_policy {
CloudPrintProxyEnabled {
CloudPrintProxyEnabled: False
}
}
}
)");
} // namespace
// 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 {}
CloudPrintProxyFrontend* frontend() { return backend_->frontend_; }
bool PostFrontendTask(const base::Location& from_here,
const base::Closure& task);
bool CurrentlyOnFrontendThread() const;
bool CurrentlyOnCoreThread() const;
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* const 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.
std::unique_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_;
std::unique_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_task_runner_(base::ThreadTaskRunnerHandle::Get()),
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;
PostCoreTask(FROM_HERE,
base::Bind(&CloudPrintProxyBackend::Core::DoInitializeWithToken,
core_, 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;
PostCoreTask(
FROM_HERE,
base::Bind(&CloudPrintProxyBackend::Core::DoInitializeWithRobotToken,
core_, 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;
PostCoreTask(
FROM_HERE,
base::Bind(&CloudPrintProxyBackend::Core::DoInitializeWithRobotAuthCode,
core_, robot_oauth_auth_code, robot_email));
return true;
}
void CloudPrintProxyBackend::Shutdown() {
PostCoreTask(FROM_HERE, base::Bind(&CloudPrintProxyBackend::Core::DoShutdown,
core_));
core_thread_.Stop();
core_ = nullptr; // Releases reference to |core_|.
}
void CloudPrintProxyBackend::UnregisterPrinters() {
PostCoreTask(FROM_HERE,
base::Bind(&CloudPrintProxyBackend::Core::DoUnregisterPrinters,
core_));
}
bool CloudPrintProxyBackend::PostCoreTask(const base::Location& from_here,
const base::Closure& task) {
return core_thread_.task_runner()->PostTask(from_here, task);
}
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);
}
bool CloudPrintProxyBackend::Core::PostFrontendTask(
const base::Location& from_here,
const base::Closure& task) {
return backend_->frontend_task_runner_->PostTask(from_here, task);
}
bool CloudPrintProxyBackend::Core::CurrentlyOnFrontendThread() const {
return backend_->frontend_task_runner_->BelongsToCurrentThread();
}
bool CloudPrintProxyBackend::Core::CurrentlyOnCoreThread() const {
return backend_->core_thread_.task_runner()->BelongsToCurrentThread();
}
void CloudPrintProxyBackend::Core::CreateAuthAndConnector() {
net::PartialNetworkTrafficAnnotationTag partial_traffic_annotation =
net::DefinePartialNetworkTrafficAnnotation("cloud_print_backend",
"cloud_print", R"(
semantics {
description:
"Creates and authenticates connection with Cloud Print."
trigger: "Cloud Print service intialization."
data: "OAuth2 token."
})");
if (!auth_.get()) {
auth_ =
new CloudPrintAuth(this, settings_.server_url(), oauth_client_info_,
settings_.proxy_id(), partial_traffic_annotation);
}
if (!connector_.get()) {
connector_ =
new CloudPrintConnector(this, settings_, partial_traffic_annotation);
}
}
void CloudPrintProxyBackend::Core::DestroyAuthAndConnector() {
auth_ = NULL;
connector_ = NULL;
}
void CloudPrintProxyBackend::Core::DoInitializeWithToken(
const std::string& cloud_print_token) {
DCHECK(CurrentlyOnCoreThread());
CreateAuthAndConnector();
auth_->AuthenticateWithToken(cloud_print_token);
}
void CloudPrintProxyBackend::Core::DoInitializeWithRobotToken(
const std::string& robot_oauth_refresh_token,
const std::string& robot_email) {
DCHECK(CurrentlyOnCoreThread());
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(CurrentlyOnCoreThread());
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.
PostFrontendTask(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,
kCloudPrintCredentialUpdateTrafficAnnotation);
}
// Start cloud print connector if needed.
if (!connector_->IsRunning()) {
if (!connector_->Start()) {
// Let the frontend know that we do not have a print system.
PostFrontendTask(FROM_HERE,
base::Bind(&Core::NotifyPrintSystemUnavailable, this));
}
}
}
void CloudPrintProxyBackend::Core::OnInvalidCredentials() {
DCHECK(CurrentlyOnCoreThread());
VLOG(1) << "CP_CONNECTOR: Auth Error";
PostFrontendTask(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);
PostFrontendTask(
FROM_HERE, base::Bind(&Core::NotifyXmppPingUpdated, this, ping_timeout));
}
void CloudPrintProxyBackend::Core::InitNotifications(
const std::string& robot_email,
const std::string& access_token) {
DCHECK(CurrentlyOnCoreThread());
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,
kCloudPrintCredentialUpdateTrafficAnnotation);
}
void CloudPrintProxyBackend::Core::DoShutdown() {
DCHECK(CurrentlyOnCoreThread());
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(CurrentlyOnCoreThread());
std::string access_token = GetTokenStore()->token();
std::list<std::string> printer_ids = connector_->GetPrinterIds();
PostFrontendTask(FROM_HERE, base::Bind(&Core::NotifyUnregisterPrinters, this,
access_token, printer_ids));
}
void CloudPrintProxyBackend::Core::HandlePrinterNotification(
const std::string& notification) {
DCHECK(CurrentlyOnCoreThread());
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(CurrentlyOnCoreThread());
// 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::BindOnce(&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::BindOnce(&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::BindOnce(&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(CurrentlyOnCoreThread());
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(CurrentlyOnFrontendThread());
frontend()->OnAuthenticated(robot_oauth_refresh_token, robot_email,
user_email);
}
void CloudPrintProxyBackend::Core::NotifyAuthenticationFailed() {
DCHECK(CurrentlyOnFrontendThread());
frontend()->OnAuthenticationFailed();
}
void CloudPrintProxyBackend::Core::NotifyPrintSystemUnavailable() {
DCHECK(CurrentlyOnFrontendThread());
frontend()->OnPrintSystemUnavailable();
}
void CloudPrintProxyBackend::Core::NotifyUnregisterPrinters(
const std::string& auth_token,
const std::list<std::string>& printer_ids) {
DCHECK(CurrentlyOnFrontendThread());
frontend()->OnUnregisterPrinters(auth_token, printer_ids);
}
void CloudPrintProxyBackend::Core::NotifyXmppPingUpdated(int ping_timeout) {
DCHECK(CurrentlyOnFrontendThread());
frontend()->OnXmppPingUpdated(ping_timeout);
}
void CloudPrintProxyBackend::Core::OnNotificationsEnabled() {
DCHECK(CurrentlyOnCoreThread());
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(CurrentlyOnCoreThread());
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) {
DCHECK(CurrentlyOnCoreThread());
// Since we got some notification from the server,
// reset pending ping counter to 0.
pending_xmpp_pings_ = 0;
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