blob: 0bcaa2050b89a64db10c84f252607cefba0ef0dc [file] [log] [blame]
// Copyright 2018 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 "chromeos/components/drivefs/drivefs_host.h"
#include <map>
#include <set>
#include <utility>
#include "base/strings/strcat.h"
#include "base/unguessable_token.h"
#include "chromeos/components/drivefs/drivefs_host_observer.h"
#include "chromeos/components/drivefs/pending_connection_manager.h"
#include "components/drive/drive_notification_manager.h"
#include "components/drive/drive_notification_observer.h"
#include "mojo/public/cpp/bindings/binding.h"
#include "mojo/public/cpp/platform/platform_channel_endpoint.h"
#include "mojo/public/cpp/system/invitation.h"
#include "net/base/network_change_notifier.h"
#include "services/identity/public/mojom/constants.mojom.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/service_manager/public/cpp/connector.h"
namespace drivefs {
namespace {
constexpr char kMountScheme[] = "drivefs://";
constexpr char kDataPath[] = "GCache/v2";
constexpr char kIdentityConsumerId[] = "drivefs";
constexpr base::TimeDelta kMountTimeout = base::TimeDelta::FromSeconds(20);
constexpr base::TimeDelta kQueryCacheTtl = base::TimeDelta::FromMinutes(5);
class MojoConnectionDelegateImpl : public DriveFsHost::MojoConnectionDelegate {
public:
MojoConnectionDelegateImpl() = default;
mojom::DriveFsBootstrapPtrInfo InitializeMojoConnection() override {
return mojom::DriveFsBootstrapPtrInfo(
invitation_.AttachMessagePipe("drivefs-bootstrap"),
mojom::DriveFsBootstrap::Version_);
}
void AcceptMojoConnection(base::ScopedFD handle) override {
mojo::OutgoingInvitation::Send(
std::move(invitation_), base::kNullProcessHandle,
mojo::PlatformChannelEndpoint(mojo::PlatformHandle(std::move(handle))));
}
private:
// The underlying mojo connection.
mojo::OutgoingInvitation invitation_;
DISALLOW_COPY_AND_ASSIGN(MojoConnectionDelegateImpl);
};
} // namespace
std::unique_ptr<OAuth2MintTokenFlow> DriveFsHost::Delegate::CreateMintTokenFlow(
OAuth2MintTokenFlow::Delegate* delegate,
const std::string& client_id,
const std::string& app_id,
const std::vector<std::string>& scopes) {
return std::make_unique<OAuth2MintTokenFlow>(
delegate, OAuth2MintTokenFlow::Parameters{
app_id, client_id, scopes, kIdentityConsumerId,
OAuth2MintTokenFlow::MODE_MINT_TOKEN_FORCE});
}
std::unique_ptr<DriveFsHost::MojoConnectionDelegate>
DriveFsHost::Delegate::CreateMojoConnectionDelegate() {
return std::make_unique<MojoConnectionDelegateImpl>();
}
class DriveFsHost::AccountTokenDelegate : public OAuth2MintTokenFlow::Delegate {
public:
explicit AccountTokenDelegate(DriveFsHost* host) : host_(host) {}
~AccountTokenDelegate() override = default;
void GetAccessToken(bool use_cached,
const std::string& client_id,
const std::string& app_id,
const std::vector<std::string>& scopes,
mojom::DriveFsDelegate::GetAccessTokenCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(host_->sequence_checker_);
if (get_access_token_callback_) {
std::move(callback).Run(mojom::AccessTokenStatus::kTransientError, "");
return;
}
DCHECK(!mint_token_flow_);
const std::string& token =
MaybeGetCachedToken(use_cached, client_id, app_id, scopes);
if (!token.empty()) {
std::move(callback).Run(mojom::AccessTokenStatus::kSuccess, token);
return;
}
get_access_token_callback_ = std::move(callback);
mint_token_flow_ =
host_->delegate_->CreateMintTokenFlow(this, client_id, app_id, scopes);
DCHECK(mint_token_flow_);
GetIdentityManager().GetPrimaryAccountWhenAvailable(base::BindOnce(
&AccountTokenDelegate::AccountReady, base::Unretained(this)));
}
private:
void AccountReady(const AccountInfo& info,
const identity::AccountState& state) {
GetIdentityManager().GetAccessToken(
host_->delegate_->GetAccountId().GetUserEmail(), {},
kIdentityConsumerId,
base::BindOnce(&AccountTokenDelegate::GotChromeAccessToken,
base::Unretained(this)));
}
void GotChromeAccessToken(const base::Optional<std::string>& access_token,
base::Time expiration_time,
const GoogleServiceAuthError& error) {
DCHECK_CALLED_ON_VALID_SEQUENCE(host_->sequence_checker_);
if (!access_token) {
OnMintTokenFailure(error);
return;
}
mint_token_flow_->Start(host_->delegate_->GetURLLoaderFactory(),
*access_token);
}
// OAuth2MintTokenFlow::Delegate:
void OnMintTokenSuccess(const std::string& access_token,
int time_to_live) override {
DCHECK_CALLED_ON_VALID_SEQUENCE(host_->sequence_checker_);
UpdateCachedToken(access_token, base::TimeDelta::FromSeconds(time_to_live));
std::move(get_access_token_callback_)
.Run(mojom::AccessTokenStatus::kSuccess, access_token);
mint_token_flow_.reset();
}
void OnMintTokenFailure(const GoogleServiceAuthError& error) override {
DCHECK_CALLED_ON_VALID_SEQUENCE(host_->sequence_checker_);
std::move(get_access_token_callback_)
.Run(error.IsPersistentError()
? mojom::AccessTokenStatus::kAuthError
: mojom::AccessTokenStatus::kTransientError,
"");
mint_token_flow_.reset();
}
const std::string& MaybeGetCachedToken(
bool use_cached,
const std::string& client_id,
const std::string& app_id,
const std::vector<std::string>& scopes) {
// Return value from cache at most once per mount.
if (!use_cached || host_->clock_->Now() >= last_token_expiry_) {
last_token_.clear();
}
return last_token_;
}
void UpdateCachedToken(const std::string& token, const base::TimeDelta& ttl) {
last_token_ = token;
last_token_expiry_ = host_->clock_->Now() + ttl;
}
identity::mojom::IdentityManager& GetIdentityManager() {
DCHECK_CALLED_ON_VALID_SEQUENCE(host_->sequence_checker_);
if (!identity_manager_) {
host_->delegate_->GetConnector()->BindInterface(
identity::mojom::kServiceName, mojo::MakeRequest(&identity_manager_));
}
return *identity_manager_;
}
// Owns |this|.
DriveFsHost* const host_;
// The connection to the identity service. Access via |GetIdentityManager()|.
identity::mojom::IdentityManagerPtr identity_manager_;
// Pending callback for an in-flight GetAccessToken request.
mojom::DriveFsDelegate::GetAccessTokenCallback get_access_token_callback_;
// The mint token flow, if one is in flight.
std::unique_ptr<OAuth2MintTokenFlow> mint_token_flow_;
std::string last_token_;
base::Time last_token_expiry_;
DISALLOW_COPY_AND_ASSIGN(AccountTokenDelegate);
};
// A container of state tied to a particular mounting of DriveFS. None of this
// should be shared between mounts.
class DriveFsHost::MountState
: public mojom::DriveFsDelegate,
public OAuth2MintTokenFlow::Delegate,
public chromeos::disks::DiskMountManager::Observer,
public drive::DriveNotificationObserver {
public:
explicit MountState(DriveFsHost* host)
: host_(host),
mojo_connection_delegate_(
host_->delegate_->CreateMojoConnectionDelegate()),
pending_token_(base::UnguessableToken::Create()),
binding_(this),
weak_ptr_factory_(this) {
host_->disk_mount_manager_->AddObserver(this);
source_path_ = base::StrCat({kMountScheme, pending_token_.ToString()});
std::string datadir_option =
base::StrCat({"datadir=", host_->GetDataPath().value()});
auto bootstrap =
mojo::MakeProxy(mojo_connection_delegate_->InitializeMojoConnection());
mojom::DriveFsDelegatePtr delegate;
binding_.Bind(mojo::MakeRequest(&delegate));
binding_.set_connection_error_handler(
base::BindOnce(&MountState::OnConnectionError, base::Unretained(this)));
bootstrap->Init(
{base::in_place, host_->delegate_->GetAccountId().GetUserEmail()},
mojo::MakeRequest(&drivefs_), std::move(delegate));
drivefs_.set_connection_error_handler(
base::BindOnce(&MountState::OnConnectionError, base::Unretained(this)));
// If unconsumed, the registration is cleaned up when |this| is destructed.
PendingConnectionManager::Get().ExpectOpenIpcChannel(
pending_token_, base::BindOnce(&MountState::AcceptMojoConnection,
base::Unretained(this)));
host_->disk_mount_manager_->MountPath(
source_path_, "",
base::StrCat({"drivefs-", host_->delegate_->GetObfuscatedAccountId()}),
{datadir_option}, chromeos::MOUNT_TYPE_NETWORK_STORAGE,
chromeos::MOUNT_ACCESS_MODE_READ_WRITE);
host_->timer_->Start(
FROM_HERE, kMountTimeout,
base::BindOnce(&MountState::OnTimedOut, base::Unretained(this)));
}
~MountState() override {
DCHECK_CALLED_ON_VALID_SEQUENCE(host_->sequence_checker_);
host_->disk_mount_manager_->RemoveObserver(this);
host_->delegate_->GetDriveNotificationManager().RemoveObserver(this);
host_->timer_->Stop();
if (pending_token_) {
PendingConnectionManager::Get().CancelExpectedOpenIpcChannel(
pending_token_);
}
if (!mount_path_.empty()) {
host_->disk_mount_manager_->UnmountPath(
mount_path_.value(), chromeos::UNMOUNT_OPTIONS_NONE, {});
}
if (mounted()) {
for (auto& observer : host_->observers_) {
observer.OnUnmounted();
}
}
}
bool mounted() const { return drivefs_has_mounted_ && !mount_path_.empty(); }
const base::FilePath& mount_path() const { return mount_path_; }
mojom::DriveFs* GetDriveFsInterface() const { return drivefs_.get(); }
// Accepts the mojo connection over |handle|, delegating to
// |mojo_connection_delegate_|.
void AcceptMojoConnection(base::ScopedFD handle) {
DCHECK_CALLED_ON_VALID_SEQUENCE(host_->sequence_checker_);
DCHECK(pending_token_);
pending_token_ = {};
mojo_connection_delegate_->AcceptMojoConnection(std::move(handle));
}
mojom::QueryParameters::QuerySource SearchDriveFs(
mojom::QueryParametersPtr query,
mojom::SearchQuery::GetNextPageCallback callback) {
// The only cacheable query is 'shared with me'.
if (IsCloudSharedWithMeQuery(query)) {
// Check if we should have the response cached.
auto delta = host_->clock_->Now() - last_shared_with_me_response_;
if (delta <= kQueryCacheTtl) {
query->query_source =
drivefs::mojom::QueryParameters::QuerySource::kLocalOnly;
}
}
drivefs::mojom::SearchQueryPtr search;
drivefs::mojom::QueryParameters::QuerySource source = query->query_source;
if (net::NetworkChangeNotifier::IsOffline() &&
source != drivefs::mojom::QueryParameters::QuerySource::kLocalOnly) {
// No point trying cloud query if we know we are offline.
source = drivefs::mojom::QueryParameters::QuerySource::kLocalOnly;
OnSearchDriveFs(std::move(search), std::move(query), std::move(callback),
drive::FILE_ERROR_NO_CONNECTION, {});
} else {
drivefs_->StartSearchQuery(mojo::MakeRequest(&search), query.Clone());
auto* raw_search = search.get();
raw_search->GetNextPage(base::BindOnce(
&MountState::OnSearchDriveFs, weak_ptr_factory_.GetWeakPtr(),
std::move(search), std::move(query), std::move(callback)));
}
return source;
}
private:
// mojom::DriveFsDelegate:
void GetAccessToken(const std::string& client_id,
const std::string& app_id,
const std::vector<std::string>& scopes,
GetAccessTokenCallback callback) override {
DCHECK_CALLED_ON_VALID_SEQUENCE(host_->sequence_checker_);
host_->account_token_delegate_->GetAccessToken(!token_fetch_attempted_,
client_id, app_id, scopes,
std::move(callback));
token_fetch_attempted_ = true;
}
void OnHeartbeat() override {
DCHECK_CALLED_ON_VALID_SEQUENCE(host_->sequence_checker_);
if (host_->timer_->IsRunning()) {
host_->timer_->Reset();
}
}
void OnMounted() override {
DCHECK_CALLED_ON_VALID_SEQUENCE(host_->sequence_checker_);
DCHECK(!drivefs_has_mounted_);
drivefs_has_mounted_ = true;
MaybeNotifyDelegateOnMounted();
}
void OnMountFailed(base::Optional<base::TimeDelta> remount_delay) override {
DCHECK_CALLED_ON_VALID_SEQUENCE(host_->sequence_checker_);
drivefs_has_mounted_ = false;
drivefs_has_terminated_ = true;
bool needs_restart = remount_delay.has_value();
host_->mount_observer_->OnMountFailed(
needs_restart ? MountObserver::MountFailure::kNeedsRestart
: MountObserver::MountFailure::kUnknown,
std::move(remount_delay));
}
void OnUnmounted(base::Optional<base::TimeDelta> remount_delay) override {
DCHECK_CALLED_ON_VALID_SEQUENCE(host_->sequence_checker_);
drivefs_has_mounted_ = false;
drivefs_has_terminated_ = true;
host_->mount_observer_->OnUnmounted(std::move(remount_delay));
}
void OnSyncingStatusUpdate(mojom::SyncingStatusPtr status) override {
for (auto& observer : host_->observers_) {
observer.OnSyncingStatusUpdate(*status);
}
}
void OnFilesChanged(std::vector<mojom::FileChangePtr> changes) override {
std::vector<mojom::FileChange> changes_values;
changes_values.reserve(changes.size());
for (auto& change : changes) {
changes_values.emplace_back(std::move(*change));
}
for (auto& observer : host_->observers_) {
observer.OnFilesChanged(changes_values);
}
}
void OnError(mojom::DriveErrorPtr error) override {
if (!IsKnownEnumValue(error->type)) {
return;
}
for (auto& observer : host_->observers_) {
observer.OnError(*error);
}
}
void OnTeamDrivesListReady(
const std::vector<std::string>& team_drive_ids) override {
host_->delegate_->GetDriveNotificationManager().AddObserver(this);
host_->delegate_->GetDriveNotificationManager().UpdateTeamDriveIds(
std::set<std::string>(team_drive_ids.begin(), team_drive_ids.end()),
{});
}
void OnTeamDriveChanged(const std::string& team_drive_id,
CreateOrDelete change_type) override {
std::set<std::string> additions;
std::set<std::string> removals;
if (change_type == mojom::DriveFsDelegate::CreateOrDelete::kCreated) {
additions.insert(team_drive_id);
} else {
removals.insert(team_drive_id);
}
host_->delegate_->GetDriveNotificationManager().UpdateTeamDriveIds(
additions, removals);
}
void OnConnectionError() {
DCHECK_CALLED_ON_VALID_SEQUENCE(host_->sequence_checker_);
if (!drivefs_has_terminated_) {
if (mounted()) {
host_->mount_observer_->OnUnmounted({});
} else {
host_->mount_observer_->OnMountFailed(
MountObserver::MountFailure::kIpcDisconnect, {});
}
drivefs_has_terminated_ = true;
}
}
void OnTimedOut() {
DCHECK_CALLED_ON_VALID_SEQUENCE(host_->sequence_checker_);
host_->timer_->Stop();
drivefs_has_mounted_ = false;
drivefs_has_terminated_ = true;
host_->mount_observer_->OnMountFailed(MountObserver::MountFailure::kTimeout,
{});
}
void MaybeNotifyDelegateOnMounted() {
if (mounted()) {
host_->timer_->Stop();
host_->mount_observer_->OnMounted(mount_path());
}
}
// DiskMountManager::Observer:
void OnMountEvent(chromeos::disks::DiskMountManager::MountEvent event,
chromeos::MountError error_code,
const chromeos::disks::DiskMountManager::MountPointInfo&
mount_info) override {
DCHECK_CALLED_ON_VALID_SEQUENCE(host_->sequence_checker_);
if (mount_info.mount_type != chromeos::MOUNT_TYPE_NETWORK_STORAGE ||
mount_info.source_path != source_path_ ||
event != chromeos::disks::DiskMountManager::MOUNTING) {
return;
}
if (error_code != chromeos::MOUNT_ERROR_NONE) {
auto* observer = host_->mount_observer_;
// Deletes |this|.
host_->Unmount();
observer->OnMountFailed(MountObserver::MountFailure::kInvocation, {});
return;
}
DCHECK(!mount_info.mount_path.empty());
mount_path_ = base::FilePath(mount_info.mount_path);
host_->disk_mount_manager_->RemoveObserver(this);
MaybeNotifyDelegateOnMounted();
}
// DriveNotificationObserver overrides:
void OnNotificationReceived(
const std::map<std::string, int64_t>& invalidations) override {
std::vector<mojom::FetchChangeLogOptionsPtr> options;
options.reserve(invalidations.size());
for (const auto& invalidation : invalidations) {
options.emplace_back(base::in_place, invalidation.second,
invalidation.first);
}
drivefs_->FetchChangeLog(std::move(options));
}
void OnNotificationTimerFired() override { drivefs_->FetchAllChangeLogs(); }
void OnSearchDriveFs(
drivefs::mojom::SearchQueryPtr search,
drivefs::mojom::QueryParametersPtr query,
mojom::SearchQuery::GetNextPageCallback callback,
drive::FileError error,
base::Optional<std::vector<drivefs::mojom::QueryItemPtr>> items) {
if (error == drive::FILE_ERROR_NO_CONNECTION &&
query->query_source !=
drivefs::mojom::QueryParameters::QuerySource::kLocalOnly) {
// Retry with offline query.
query->query_source =
drivefs::mojom::QueryParameters::QuerySource::kLocalOnly;
if (query->text_content) {
// Full-text searches not supported offline.
std::swap(query->text_content, query->title);
query->text_content.reset();
}
SearchDriveFs(std::move(query), std::move(callback));
return;
}
if (IsCloudSharedWithMeQuery(query)) {
// Mark that DriveFS should have cached the required info.
last_shared_with_me_response_ = host_->clock_->Now();
}
std::move(callback).Run(error, std::move(items));
}
static bool IsCloudSharedWithMeQuery(
const drivefs::mojom::QueryParametersPtr& query) {
return query->query_source ==
drivefs::mojom::QueryParameters::QuerySource::kCloudOnly &&
query->shared_with_me && !query->text_content && !query->title;
}
// Owns |this|.
DriveFsHost* const host_;
const std::unique_ptr<DriveFsHost::MojoConnectionDelegate>
mojo_connection_delegate_;
// The token passed to DriveFS as part of |source_path_| used to match it to
// this DriveFsHost instance.
base::UnguessableToken pending_token_;
// The path passed to cros-disks to mount.
std::string source_path_;
// The path where DriveFS is mounted.
base::FilePath mount_path_;
// Mojo connections to the DriveFS process.
mojom::DriveFsPtr drivefs_;
mojo::Binding<mojom::DriveFsDelegate> binding_;
bool drivefs_has_mounted_ = false;
bool drivefs_has_terminated_ = false;
bool token_fetch_attempted_ = false;
base::Time last_shared_with_me_response_;
base::WeakPtrFactory<MountState> weak_ptr_factory_;
DISALLOW_COPY_AND_ASSIGN(MountState);
};
DriveFsHost::DriveFsHost(const base::FilePath& profile_path,
DriveFsHost::Delegate* delegate,
DriveFsHost::MountObserver* mount_observer,
const base::Clock* clock,
chromeos::disks::DiskMountManager* disk_mount_manager,
std::unique_ptr<base::OneShotTimer> timer)
: profile_path_(profile_path),
delegate_(delegate),
mount_observer_(mount_observer),
clock_(clock),
disk_mount_manager_(disk_mount_manager),
timer_(std::move(timer)),
account_token_delegate_(std::make_unique<AccountTokenDelegate>(this)) {
DCHECK(delegate_);
DCHECK(mount_observer_);
DCHECK(clock_);
}
DriveFsHost::~DriveFsHost() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}
void DriveFsHost::AddObserver(DriveFsHostObserver* observer) {
observers_.AddObserver(observer);
}
void DriveFsHost::RemoveObserver(DriveFsHostObserver* observer) {
observers_.RemoveObserver(observer);
}
bool DriveFsHost::Mount() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
const AccountId& account_id = delegate_->GetAccountId();
if (mount_state_ || !account_id.HasAccountIdKey() ||
account_id.GetUserEmail().empty()) {
return false;
}
mount_state_ = std::make_unique<MountState>(this);
return true;
}
void DriveFsHost::Unmount() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
mount_state_.reset();
}
bool DriveFsHost::IsMounted() const {
return mount_state_ && mount_state_->mounted();
}
base::FilePath DriveFsHost::GetMountPath() const {
return mount_state_ && mount_state_->mounted()
? mount_state_->mount_path()
: base::FilePath("/media/fuse")
.Append(base::StrCat(
{"drivefs-", delegate_->GetObfuscatedAccountId()}));
}
base::FilePath DriveFsHost::GetDataPath() const {
return profile_path_.Append(kDataPath).Append(
delegate_->GetObfuscatedAccountId());
}
mojom::DriveFs* DriveFsHost::GetDriveFsInterface() const {
if (!mount_state_ || !mount_state_->mounted()) {
return nullptr;
}
return mount_state_->GetDriveFsInterface();
}
mojom::QueryParameters::QuerySource DriveFsHost::PerformSearch(
mojom::QueryParametersPtr query,
mojom::SearchQuery::GetNextPageCallback callback) {
return mount_state_->SearchDriveFs(std::move(query), std::move(callback));
}
} // namespace drivefs