Commit 13e78315 authored by Yuwei Huang's avatar Yuwei Huang Committed by Commit Bot

[remoting][FTL][iOS] Integrate with new directory service

This CL integrates the iOS client with the new directory service. It
also deletes the HostListFetcher and makes HostListService store and
return the protobuf HostInfo.

Bug: 954594
Change-Id: Ia47d91ba5e7bc2d0fdbdb441c95694e75deab6f4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1602075
Commit-Queue: Joe Downing <joedow@chromium.org>
Reviewed-by: default avatarJoe Downing <joedow@chromium.org>
Cr-Commit-Position: refs/heads/master@{#658563}
parent 42c2ca28
...@@ -7,7 +7,9 @@ ...@@ -7,7 +7,9 @@
#include <memory> #include <memory>
#include "third_party/grpc/src/include/grpcpp/channel.h" namespace grpc {
class ChannelInterface;
} // namespace grpc
namespace remoting { namespace remoting {
......
...@@ -2,4 +2,4 @@ ...@@ -2,4 +2,4 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
using GrpcChannelSharedPtr = std::shared_ptr<grpc::Channel>; using GrpcChannelSharedPtr = std::shared_ptr<grpc::ChannelInterface>;
...@@ -165,7 +165,7 @@ static NSDictionary<NSString*, NSNumber*>* const kOfflineReasonL10nId = @{ ...@@ -165,7 +165,7 @@ static NSDictionary<NSString*, NSNumber*>* const kOfflineReasonL10nId = @{
_imageView.image = RemotingTheme.desktopIcon; _imageView.image = RemotingTheme.desktopIcon;
if ([_hostInfo.status isEqualToString:@"ONLINE"]) { if (_hostInfo.isOnline) {
_imageView.backgroundColor = RemotingTheme.hostOnlineColor; _imageView.backgroundColor = RemotingTheme.hostOnlineColor;
_statusLabel.text = l10n_util::GetNSString(IDS_HOST_ONLINE_SUBTITLE); _statusLabel.text = l10n_util::GetNSString(IDS_HOST_ONLINE_SUBTITLE);
} else { } else {
......
...@@ -18,5 +18,9 @@ source_set("domain") { ...@@ -18,5 +18,9 @@ source_set("domain") {
"user_info.mm", "user_info.mm",
] ]
deps = [
"//remoting/proto/remoting/v1:directory_proto",
]
configs += [ "//build/config/compiler:enable_arc" ] configs += [ "//build/config/compiler:enable_arc" ]
} }
...@@ -8,7 +8,11 @@ ...@@ -8,7 +8,11 @@
#import <Foundation/Foundation.h> #import <Foundation/Foundation.h>
namespace remoting { namespace remoting {
struct HostInfo; namespace apis {
namespace v1 {
class HostInfo;
} // namespace v1
} // namespace apis
} // namespace remoting } // namespace remoting
// A detail record for a Remoting Host. // A detail record for a Remoting Host.
...@@ -22,19 +26,15 @@ struct HostInfo; ...@@ -22,19 +26,15 @@ struct HostInfo;
@property(nonatomic, copy) NSString* hostOsVersion; @property(nonatomic, copy) NSString* hostOsVersion;
@property(nonatomic, copy) NSString* hostVersion; @property(nonatomic, copy) NSString* hostVersion;
@property(nonatomic, copy) NSString* jabberId; @property(nonatomic, copy) NSString* jabberId;
@property(nonatomic, copy) NSString* ftlId;
@property(nonatomic, copy) NSString* kind; @property(nonatomic, copy) NSString* kind;
@property(nonatomic, copy) NSString* publicKey; @property(nonatomic, copy) NSString* publicKey;
@property(nonatomic, copy) NSString* status;
@property(nonatomic, copy) NSString* updatedTime; @property(nonatomic, copy) NSString* updatedTime;
@property(nonatomic, copy) NSString* offlineReason; @property(nonatomic, copy) NSString* offlineReason;
// True when |status| is @"ONLINE", anything else is False. @property(nonatomic) BOOL isOnline;
@property(nonatomic, readonly) bool isOnline;
- (instancetype)initWithRemotingHostInfo:(const remoting::HostInfo&)hostInfo; - (instancetype)initWithRemotingHostInfo:
(const remoting::apis::v1::HostInfo&)hostInfo;
// First consider if |isOnline| is greater than anything else, then consider by
// case insensitive locale of |hostName|.
- (NSComparisonResult)compare:(HostInfo*)host;
@end @end
......
...@@ -10,7 +10,8 @@ ...@@ -10,7 +10,8 @@
#include "base/i18n/time_formatting.h" #include "base/i18n/time_formatting.h"
#include "base/strings/sys_string_conversions.h" #include "base/strings/sys_string_conversions.h"
#include "remoting/ios/facade/host_info.h" #include "base/time/time.h"
#include "remoting/proto/remoting/v1/host_info.pb.h"
@implementation HostInfo @implementation HostInfo
...@@ -23,54 +24,38 @@ ...@@ -23,54 +24,38 @@
@synthesize jabberId = _jabberId; @synthesize jabberId = _jabberId;
@synthesize kind = _kind; @synthesize kind = _kind;
@synthesize publicKey = _publicKey; @synthesize publicKey = _publicKey;
@synthesize status = _status;
@synthesize updatedTime = _updatedTime; @synthesize updatedTime = _updatedTime;
@synthesize offlineReason = _offlineReason; @synthesize offlineReason = _offlineReason;
@synthesize isOnline = _isOnline;
- (instancetype)initWithRemotingHostInfo:(const remoting::HostInfo&)hostInfo { - (instancetype)initWithRemotingHostInfo:
(const remoting::apis::v1::HostInfo&)hostInfo {
if (self = [super init]) { if (self = [super init]) {
NSString* status; _hostId = base::SysUTF8ToNSString(hostInfo.host_id());
switch (hostInfo.status) { _hostName = base::SysUTF8ToNSString(hostInfo.host_name());
case remoting::kHostStatusOnline: _hostOs = base::SysUTF8ToNSString(hostInfo.host_os_name());
status = @"ONLINE"; _hostOsVersion = base::SysUTF8ToNSString(hostInfo.host_os_version());
break; _hostVersion = base::SysUTF8ToNSString(hostInfo.host_version());
case remoting::kHostStatusOffline: _jabberId = base::SysUTF8ToNSString(hostInfo.jabber_id());
status = @"OFFLINE"; _ftlId = base::SysUTF8ToNSString(hostInfo.ftl_id());
break; _publicKey = base::SysUTF8ToNSString(hostInfo.public_key());
default:
NOTREACHED(); base::Time last_seen_time =
} base::Time::UnixEpoch() +
_hostId = base::SysUTF8ToNSString(hostInfo.host_id); base::TimeDelta::FromMilliseconds(hostInfo.last_seen_time());
_hostName = base::SysUTF8ToNSString(hostInfo.host_name);
_hostOs = base::SysUTF8ToNSString(hostInfo.host_os);
_hostOsVersion = base::SysUTF8ToNSString(hostInfo.host_os_version);
_hostVersion = base::SysUTF8ToNSString(hostInfo.host_version);
_jabberId = base::SysUTF8ToNSString(hostInfo.host_jid);
_publicKey = base::SysUTF8ToNSString(hostInfo.public_key);
_status = status;
_updatedTime = base::SysUTF16ToNSString( _updatedTime = base::SysUTF16ToNSString(
base::TimeFormatShortDateAndTime(hostInfo.updated_time)); base::TimeFormatShortDateAndTime(last_seen_time));
_offlineReason = base::SysUTF8ToNSString(hostInfo.offline_reason);
}
return self;
}
- (bool)isOnline { _offlineReason = base::SysUTF8ToNSString(hostInfo.host_offline_reason());
return (self.status && [self.status isEqualToString:@"ONLINE"]); _isOnline = hostInfo.status() == remoting::apis::v1::HostInfo_Status_ONLINE;
}
- (NSComparisonResult)compare:(HostInfo*)host {
if (self.isOnline != host.isOnline) {
return self.isOnline ? NSOrderedAscending : NSOrderedDescending;
} else {
return [self.hostName localizedCaseInsensitiveCompare:_hostName];
} }
return self;
} }
- (NSString*)description { - (NSString*)description {
return return
[NSString stringWithFormat:@"HostInfo: name=%@ status=%@ updatedTime= %@", [NSString stringWithFormat:@"HostInfo: name=%@ online=%@ updatedTime= %@",
_hostName, _status, _updatedTime]; _hostName, @(_isOnline), _updatedTime];
} }
@end @end
...@@ -8,12 +8,10 @@ import("//remoting/build/config/remoting_build.gni") ...@@ -8,12 +8,10 @@ import("//remoting/build/config/remoting_build.gni")
source_set("facade") { source_set("facade") {
sources = [ sources = [
"directory_client.cc",
"directory_client.h",
"ftl_device_id_provider_ios.h", "ftl_device_id_provider_ios.h",
"ftl_device_id_provider_ios.mm", "ftl_device_id_provider_ios.mm",
"host_info.cc",
"host_info.h",
"host_list_fetcher.cc",
"host_list_fetcher.h",
"host_list_service.h", "host_list_service.h",
"host_list_service.mm", "host_list_service.mm",
"ios_client_runtime_delegate.h", "ios_client_runtime_delegate.h",
...@@ -34,19 +32,14 @@ source_set("facade") { ...@@ -34,19 +32,14 @@ source_set("facade") {
"//remoting/client", "//remoting/client",
"//remoting/ios/domain", "//remoting/ios/domain",
"//remoting/ios/persistence", "//remoting/ios/persistence",
"//remoting/proto/remoting/v1:directory_grpc_library",
"//remoting/resources", "//remoting/resources",
"//remoting/signaling", "//remoting/signaling",
"//ui/base", "//ui/base",
] ]
configs += [ "//build/config/compiler:enable_arc" ] public_deps = [
} "//remoting/proto/remoting/v1:directory_proto",
source_set("test_support") {
testonly = true
sources = [
"fake_host_list_fetcher.cc",
"fake_host_list_fetcher.h",
] ]
configs += [ "//build/config/compiler:enable_arc" ] configs += [ "//build/config/compiler:enable_arc" ]
...@@ -59,7 +52,8 @@ source_set("unit_tests") { ...@@ -59,7 +52,8 @@ source_set("unit_tests") {
] ]
deps = [ deps = [
":facade", ":facade",
":test_support", "//remoting/base/grpc_support:test_support",
"//remoting/proto/remoting/v1:directory_grpc_library",
"//testing/gmock", "//testing/gmock",
"//testing/gtest", "//testing/gtest",
"//third_party/ocmock", "//third_party/ocmock",
......
// Copyright 2019 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 "remoting/ios/facade/directory_client.h"
#include "remoting/base/grpc_support/grpc_async_unary_request.h"
#include "remoting/base/grpc_support/grpc_authenticated_executor.h"
#include "remoting/base/service_urls.h"
namespace remoting {
DirectoryClient::DirectoryClient(OAuthTokenGetter* token_getter)
: DirectoryClient(
std::make_unique<GrpcAuthenticatedExecutor>(token_getter),
CreateSslChannelForEndpoint(
ServiceUrls::GetInstance()->remoting_server_endpoint())) {}
DirectoryClient::DirectoryClient(std::unique_ptr<GrpcExecutor> executor,
GrpcChannelSharedPtr channel)
: grpc_executor_(std::move(executor)),
stub_(DirectoryService::NewStub(channel)) {}
DirectoryClient::~DirectoryClient() = default;
void DirectoryClient::GetHostList(GetHostListCallback callback) {
grpc_executor_->ExecuteRpc(CreateGrpcAsyncUnaryRequest(
base::BindOnce(&DirectoryService::Stub::AsyncGetHostList,
base::Unretained(stub_.get())),
std::make_unique<grpc::ClientContext>(), apis::v1::GetHostListRequest(),
std::move(callback)));
}
void DirectoryClient::CancelPendingRequests() {
grpc_executor_->CancelPendingRequests();
}
} // namespace remoting
// Copyright 2019 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.
#ifndef REMOTING_IOS_FACADE_DIRECTORY_CLIENT_H_
#define REMOTING_IOS_FACADE_DIRECTORY_CLIENT_H_
#include <memory>
#include "base/callback_forward.h"
#include "base/macros.h"
#include "remoting/base/grpc_support/grpc_channel.h"
#include "remoting/proto/remoting/v1/directory_service.grpc.pb.h"
namespace remoting {
class GrpcExecutor;
class OAuthTokenGetter;
// A gRPC client that communicates with the directory service.
class DirectoryClient final {
public:
using GetHostListCallback =
base::OnceCallback<void(const grpc::Status&,
const apis::v1::GetHostListResponse&)>;
// Creates a client that connects to the default server endpoint.
explicit DirectoryClient(OAuthTokenGetter* token_getter);
// Creates a client with custom executor and channel. Useful for testing.
DirectoryClient(std::unique_ptr<GrpcExecutor> executor,
GrpcChannelSharedPtr channel);
~DirectoryClient();
void GetHostList(GetHostListCallback callback);
void CancelPendingRequests();
private:
using DirectoryService = apis::v1::RemotingDirectoryService;
std::unique_ptr<GrpcExecutor> grpc_executor_;
std::unique_ptr<DirectoryService::Stub> stub_;
DISALLOW_COPY_AND_ASSIGN(DirectoryClient);
};
} // namespace remoting
#endif // REMOTING_IOS_FACADE_DIRECTORY_CLIENT_H_
// 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 "remoting/ios/facade/fake_host_list_fetcher.h"
namespace remoting {
FakeHostListFetcher::FakeHostListFetcher() : HostListFetcher(nullptr) {}
FakeHostListFetcher::~FakeHostListFetcher() {}
void FakeHostListFetcher::RetrieveHostlist(const std::string& access_token,
HostlistCallback callback) {
DCHECK(!callback_);
callback_ = std::move(callback);
}
void FakeHostListFetcher::ResolveCallback(
int response_code,
const std::vector<HostInfo>& host_list) {
DCHECK(callback_);
std::move(callback_).Run(response_code, host_list);
}
void FakeHostListFetcher::CancelFetch() {
DCHECK(cancel_fetch_expected_);
cancel_fetch_expected_ = false;
if (callback_) {
std::move(callback_).Run(RESPONSE_CODE_CANCELLED, {});
}
}
void FakeHostListFetcher::ExpectCancelFetch() {
DCHECK(!cancel_fetch_expected_);
cancel_fetch_expected_ = true;
}
} // namespace remoting
// 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.
#ifndef REMOTING_IOS_FACADE_FAKE_HOST_LIST_FETCHER_H_
#define REMOTING_IOS_FACADE_FAKE_HOST_LIST_FETCHER_H_
#include "base/macros.h"
#include "remoting/ios/facade/host_list_fetcher.h"
namespace remoting {
// Used to fake retrieving the host list without making any actual calls to the
// directory service for information.
class FakeHostListFetcher : public HostListFetcher {
public:
FakeHostListFetcher();
~FakeHostListFetcher() override;
// HostListFetcher interface.
void RetrieveHostlist(const std::string& access_token,
HostlistCallback callback) override;
void ResolveCallback(int response_code,
const std::vector<HostInfo>& host_list);
void CancelFetch() override;
void ExpectCancelFetch();
private:
HostlistCallback callback_;
bool cancel_fetch_expected_ = false;
DISALLOW_COPY_AND_ASSIGN(FakeHostListFetcher);
};
} // namespace remoting
#endif // REMOTING_IOS_FACADE_FAKE_HOST_LIST_FETCHER_H_
// Copyright 2017 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 "remoting/ios/facade/host_info.h"
#include "base/logging.h"
namespace remoting {
HostInfo::HostInfo() {}
HostInfo::HostInfo(const HostInfo& other) = default;
HostInfo::~HostInfo() {}
bool HostInfo::ParseHostInfo(const base::Value& host_info) {
// Add TokenUrlPatterns to HostInfo.
const base::Value* list_value = host_info.FindListKey("tokenUrlPatterns");
if (list_value) {
for (const base::Value& item : list_value->GetList()) {
if (!item.is_string()) {
return false;
}
token_url_patterns.push_back(item.GetString());
}
}
const std::string* string_value;
string_value = host_info.FindStringKey("status");
if (string_value && *string_value == "ONLINE") {
status = kHostStatusOnline;
} else if (string_value && *string_value == "OFFLINE") {
status = kHostStatusOffline;
} else {
LOG(ERROR) << "Unknown response status: "
<< (string_value ? *string_value : "<unset>");
return false;
}
string_value = host_info.FindStringKey("hostId");
if (string_value) {
host_id = *string_value;
} else {
LOG(ERROR) << "hostId was not found in host_info";
return false;
}
string_value = host_info.FindStringKey("hostName");
if (string_value) {
host_name = *string_value;
} else {
LOG(ERROR) << "hostName was not found in host_info";
return false;
}
string_value = host_info.FindStringKey("publicKey");
if (string_value) {
public_key = *string_value;
} else {
LOG(ERROR) << "publicKey was not found for " << host_name;
return false;
}
// If the host entry was created but the host was never online, then the jid
// is never set.
string_value = host_info.FindStringKey("jabberId");
if (string_value) {
host_jid = *string_value;
} else if (status == kHostStatusOnline) {
LOG(ERROR) << host_name << " is online but is missing a jabberId";
return false;
}
string_value = host_info.FindStringKey("updatedTime");
if (string_value) {
if (!base::Time::FromString(string_value->c_str(), &updated_time)) {
LOG(WARNING) << "Failed to parse updatedTime";
}
}
string_value = host_info.FindStringKey("hostOs");
if (string_value)
host_os = *string_value;
string_value = host_info.FindStringKey("hostOsVersion");
if (string_value)
host_os_version = *string_value;
string_value = host_info.FindStringKey("hostVersion");
if (string_value)
host_version = *string_value;
string_value = host_info.FindStringKey("hostOfflineReason");
if (string_value)
offline_reason = *string_value;
return true;
}
bool HostInfo::IsReadyForConnection() const {
return !host_jid.empty() && status == kHostStatusOnline;
}
} // namespace remoting
// Copyright 2017 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.
#ifndef REMOTING_IOS_FACADE_HOST_INFO_H_
#define REMOTING_IOS_FACADE_HOST_INFO_H_
#include <string>
#include <vector>
#include "base/time/time.h"
#include "base/values.h"
namespace remoting {
enum HostStatus { kHostStatusOnline, kHostStatusOffline };
// |remoting::HostInfo| is an object containing the data from host list fetch
// for transport in native code.
struct HostInfo {
HostInfo();
HostInfo(const HostInfo& other);
~HostInfo();
// Returns true if |host_info| is valid and initializes HostInfo.
bool ParseHostInfo(const base::Value& host_info);
// Returns true if the chromoting host is ready to accept connections.
bool IsReadyForConnection() const;
std::string host_id;
std::string host_jid;
std::string host_name;
std::string host_os;
std::string host_os_version;
std::string host_version;
HostStatus status = kHostStatusOffline;
std::string offline_reason;
std::string public_key;
base::Time updated_time;
std::vector<std::string> token_url_patterns;
};
} // namespace remoting
#endif // REMOTING_IOS_FACADE_HOST_INFO_H_
// Copyright 2017 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 "remoting/ios/facade/host_list_fetcher.h"
#include <algorithm>
#include <thread>
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/json/json_reader.h"
#include "base/logging.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/values.h"
#include "net/http/http_status_code.h"
#include "net/url_request/url_fetcher.h"
#include "remoting/base/url_request_context_getter.h"
namespace remoting {
static_assert(static_cast<int>(net::URLFetcher::RESPONSE_CODE_INVALID) !=
static_cast<int>(
HostListFetcher::ResponseCode::RESPONSE_CODE_CANCELLED),
"RESPONSE_CODE_INVALID collided with RESPONSE_CODE_CANCELLED.");
namespace {
// Used by the HostlistFetcher to make HTTP requests and also by the
// unittests for this class to set fake response data for these URLs.
// TODO(nicholss): Consider moving this to an extern and conditionally include
// prod or test environment urls based on config. A test env app would be nice.
const char kHostListProdRequestUrl[] =
"https://www.googleapis.com/chromoting/v1/@me/hosts";
// Returns true if |h1| should sort before |h2|.
bool compareHost(const HostInfo& h1, const HostInfo& h2) {
// Online hosts always sort before offline hosts.
if (h1.status != h2.status) {
return h1.status == HostStatus::kHostStatusOnline;
}
// Sort by host name.
int name_compare = h1.host_name.compare(h2.host_name);
if (name_compare != 0) {
return name_compare < 0;
}
// Sort by last update time if names are identical.
return h1.updated_time < h2.updated_time;
}
} // namespace
HostListFetcher::HostListFetcher(
const scoped_refptr<net::URLRequestContextGetter>&
url_request_context_getter)
: url_request_context_getter_(url_request_context_getter) {}
HostListFetcher::~HostListFetcher() {}
// TODO(nicholss): This was written assuming only one request at a time. Fix
// that. For the moment it will work to make progress in the app.
void HostListFetcher::RetrieveHostlist(const std::string& access_token,
HostlistCallback callback) {
// TODO(nicholss): There is a bug here if two host list fetches are happening
// at the same time there will be a dcheck thrown. Fix this for release.
DCHECK(!access_token.empty());
DCHECK(callback);
DCHECK(!hostlist_callback_);
hostlist_callback_ = std::move(callback);
request_ = net::URLFetcher::Create(GURL(kHostListProdRequestUrl),
net::URLFetcher::GET, this);
request_->SetRequestContext(url_request_context_getter_.get());
request_->AddExtraRequestHeader("Authorization: OAuth " + access_token);
request_->SetMaxRetriesOn5xx(0);
request_->SetAutomaticallyRetryOnNetworkChanges(3);
request_->Start();
}
void HostListFetcher::CancelFetch() {
request_.reset();
if (hostlist_callback_) {
std::move(hostlist_callback_).Run(RESPONSE_CODE_CANCELLED, {});
}
}
bool HostListFetcher::ProcessResponse(
std::vector<remoting::HostInfo>* hostlist) {
int response_code = request_->GetResponseCode();
if (response_code != net::HTTP_OK) {
LOG(ERROR) << "Hostlist request failed with error code: " << response_code;
return false;
}
std::string response_string;
if (!request_->GetResponseAsString(&response_string)) {
LOG(ERROR) << "Failed to retrieve Hostlist response data";
return false;
}
base::Optional<base::Value> response =
base::JSONReader::Read(response_string);
if (!response) {
LOG(ERROR) << "Failed to parse response string to JSON";
return false;
}
if (!response->is_dict()) {
LOG(ERROR) << "Parsed JSON is not a dictionary";
return false;
}
const base::Value* data = response->FindDictKey("data");
if (!data) {
LOG(ERROR) << "Hostlist response data is empty";
return false;
}
const base::Value* hosts = data->FindListKey("items");
if (!hosts) {
// This will happen if the user has no host.
return true;
}
// Any host_info with malformed data will not be added to the hostlist.
for (const base::Value& host_info : hosts->GetList()) {
remoting::HostInfo host;
if (host_info.is_dict() && host.ParseHostInfo(host_info)) {
hostlist->push_back(host);
}
}
return true;
}
void HostListFetcher::OnURLFetchComplete(const net::URLFetcher* source) {
DCHECK(source);
std::vector<HostInfo> hostlist;
if (!ProcessResponse(&hostlist)) {
hostlist.clear();
}
std::sort(hostlist.begin(), hostlist.end(), &compareHost);
std::move(hostlist_callback_).Run(request_->GetResponseCode(), hostlist);
}
} // namespace remoting
// Copyright 2017 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.
#ifndef REMOTING_IOS_FACADE_HOST_LIST_FETCHER_H_
#define REMOTING_IOS_FACADE_HOST_LIST_FETCHER_H_
#include <memory>
#include <string>
#include <vector>
#include "base/callback.h"
#include "base/macros.h"
#include "base/memory/ref_counted.h"
#include "net/url_request/url_fetcher_delegate.h"
#include "net/url_request/url_request_context_getter.h"
#include "remoting/ios/facade/host_info.h"
namespace remoting {
// Requests a host list from the directory service for an access token.
// Destroying the RemoteHostInfoFetcher while a request is outstanding
// will cancel the request. It is safe to delete the fetcher from within a
// completion callback. Must be used from a thread running a message loop.
// The public method is virtual to allow for mocking and fakes.
class HostListFetcher : public net::URLFetcherDelegate {
public:
// Imposible http response code. Used to signal that the request has been
// cancelled.
// TODO(yuweih): Add all response code values here and make the callback
// return this enum instead of int.
enum ResponseCode {
RESPONSE_CODE_CANCELLED = -257,
};
HostListFetcher(const scoped_refptr<net::URLRequestContextGetter>&
url_request_context_getter);
~HostListFetcher() override;
// Supplied by the client for each hostlist request and returns a valid,
// initialized Hostlist object on success.
typedef base::OnceCallback<
void(int response_code, const std::vector<remoting::HostInfo>& hostlist)>
HostlistCallback;
// Makes a service call to retrieve a hostlist. The
// callback will be called once the HTTP request has completed.
virtual void RetrieveHostlist(const std::string& access_token,
HostlistCallback callback);
// Cancels the current fetch request and runs the host list callback with
// RESPONSE_CODE_CANCELLED. Nothing will happen if there is no ongoing fetch
// request.
virtual void CancelFetch();
private:
// Processes the response from the directory service.
bool ProcessResponse(std::vector<remoting::HostInfo>* hostlist);
// net::URLFetcherDelegate interface.
void OnURLFetchComplete(const net::URLFetcher* source) override;
scoped_refptr<net::URLRequestContextGetter> url_request_context_getter_;
// Holds the URLFetcher for the Host List request.
std::unique_ptr<net::URLFetcher> request_;
// Caller-supplied callback used to return hostlist on success.
HostlistCallback hostlist_callback_;
DISALLOW_COPY_AND_ASSIGN(HostListFetcher);
};
} // namespace remoting
#endif // REMOTING_IOS_FACADE_HOST_LIST_FETCHER_H_
...@@ -14,11 +14,16 @@ ...@@ -14,11 +14,16 @@
#include "base/memory/weak_ptr.h" #include "base/memory/weak_ptr.h"
#include "base/no_destructor.h" #include "base/no_destructor.h"
#include "net/http/http_status_code.h" #include "net/http/http_status_code.h"
#include "remoting/ios/facade/host_info.h" #include "remoting/proto/remoting/v1/directory_messages.pb.h"
namespace grpc {
class Status;
} // namespace grpc
namespace remoting { namespace remoting {
class HostListFetcher; class DirectoryClient;
class OAuthTokenGetter;
// |HostListService| is the centralized place to retrieve the current signed in // |HostListService| is the centralized place to retrieve the current signed in
// user's host list. // user's host list.
...@@ -36,13 +41,11 @@ class HostListService { ...@@ -36,13 +41,11 @@ class HostListService {
enum class FetchFailureReason { enum class FetchFailureReason {
NETWORK_ERROR, NETWORK_ERROR,
AUTH_ERROR, AUTH_ERROR,
REQUEST_ERROR, UNKNOWN_ERROR,
}; };
struct FetchFailureInfo { struct FetchFailureInfo {
FetchFailureReason reason; FetchFailureReason reason;
// error_code is only used if the reason is |REQUEST_ERROR|.
int error_code;
std::string localized_description; std::string localized_description;
}; };
...@@ -67,7 +70,7 @@ class HostListService { ...@@ -67,7 +70,7 @@ class HostListService {
// Returns the host list. Returns an empty vector if the host list state is // Returns the host list. Returns an empty vector if the host list state is
// not |FETCHED|. // not |FETCHED|.
const std::vector<remoting::HostInfo>& hosts() const { return hosts_; } const std::vector<apis::v1::HostInfo>& hosts() const { return hosts_; }
State state() const { return state_; } State state() const { return state_; }
...@@ -77,26 +80,23 @@ class HostListService { ...@@ -77,26 +80,23 @@ class HostListService {
return last_fetch_failure_.get(); return last_fetch_failure_.get();
} }
// Allow creating instace for each test so that states don't get carried over.
static std::unique_ptr<HostListService> CreateInstanceForTesting();
void SetHostListFetcherForTesting(std::unique_ptr<HostListFetcher> fetcher);
private: private:
friend class base::NoDestructor<HostListService>; friend class base::NoDestructor<HostListService>;
friend std::unique_ptr<HostListService> std::make_unique<HostListService>(); friend class HostListServiceTest;
HostListService(); HostListService();
// Changes the host list state and notifies callbacks. // For test.
void SetState(State state); explicit HostListService(std::unique_ptr<DirectoryClient> directory_client);
void StartHostListFetch(const std::string& access_token); void Init();
void HandleHostListResult(int responseCode, // Changes the host list state and notifies callbacks.
const std::vector<remoting::HostInfo>& hostlist); void SetState(State state);
void HandleFetchFailure(FetchFailureReason reason, int error_code); void HandleHostListResult(const grpc::Status& status,
const apis::v1::GetHostListResponse& response);
void HandleFetchFailure(const grpc::Status& status);
void OnUserUpdated(bool is_user_signed_in); void OnUserUpdated(bool is_user_signed_in);
...@@ -105,9 +105,10 @@ class HostListService { ...@@ -105,9 +105,10 @@ class HostListService {
base::CallbackList<void()> host_list_state_callbacks_; base::CallbackList<void()> host_list_state_callbacks_;
base::CallbackList<void()> fetch_failure_callbacks_; base::CallbackList<void()> fetch_failure_callbacks_;
std::unique_ptr<HostListFetcher> host_list_fetcher_; std::unique_ptr<OAuthTokenGetter> token_getter_;
std::unique_ptr<DirectoryClient> directory_client_;
std::vector<remoting::HostInfo> hosts_; std::vector<apis::v1::HostInfo> hosts_;
State state_ = State::NOT_FETCHED; State state_ = State::NOT_FETCHED;
std::unique_ptr<FetchFailureInfo> last_fetch_failure_; std::unique_ptr<FetchFailureInfo> last_fetch_failure_;
......
...@@ -10,51 +10,51 @@ ...@@ -10,51 +10,51 @@
#import <CoreFoundation/CoreFoundation.h> #import <CoreFoundation/CoreFoundation.h>
#import "remoting/ios/domain/host_info.h" #include <algorithm>
#import "remoting/ios/domain/user_info.h" #import "remoting/ios/domain/user_info.h"
#import "remoting/ios/facade/host_info.h"
#import "remoting/ios/facade/host_list_fetcher.h"
#import "remoting/ios/facade/remoting_authentication.h" #import "remoting/ios/facade/remoting_authentication.h"
#import "remoting/ios/facade/remoting_service.h" #import "remoting/ios/facade/remoting_service.h"
#include "base/bind.h" #include "base/bind.h"
#include "base/i18n/time_formatting.h"
#include "base/logging.h" #include "base/logging.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "net/url_request/url_fetcher.h"
#include "remoting/base/string_resources.h" #include "remoting/base/string_resources.h"
#include "remoting/client/chromoting_client_runtime.h"
#include "remoting/ios/facade/directory_client.h"
#include "ui/base/l10n/l10n_util.h" #include "ui/base/l10n/l10n_util.h"
namespace remoting { namespace remoting {
namespace { namespace {
bool IsValidErrorCode(int error_code) { HostListService::FetchFailureReason MapError(grpc::StatusCode status_code) {
#define HTTP_STATUS(label, code, reason) \ switch (status_code) {
if (error_code == code) \ case grpc::StatusCode::UNAVAILABLE:
return true; case grpc::StatusCode::DEADLINE_EXCEEDED:
#include "net/http/http_status_code_list.h" return HostListService::FetchFailureReason::NETWORK_ERROR;
#undef HTTP_STATUS case grpc::StatusCode::PERMISSION_DENIED:
return false; case grpc::StatusCode::UNAUTHENTICATED:
return HostListService::FetchFailureReason::AUTH_ERROR;
default:
return HostListService::FetchFailureReason::UNKNOWN_ERROR;
}
} }
std::string GetRequestErrorMessage(int error_code) { // Returns true if |h1| should sort before |h2|.
if (IsValidErrorCode(error_code)) { bool CompareHost(const apis::v1::HostInfo& h1, const apis::v1::HostInfo& h2) {
std::string error_phrase = // Online hosts always sort before offline hosts.
net::GetHttpReasonPhrase(static_cast<net::HttpStatusCode>(error_code)); if (h1.status() != h2.status()) {
return l10n_util::GetStringFUTF8(IDS_SERVER_COMMUNICATION_ERROR, return h1.status() == apis::v1::HostInfo_Status_ONLINE;
base::UTF8ToUTF16(error_phrase));
} }
switch (error_code) { // Sort by host name.
case net::URLFetcher::RESPONSE_CODE_INVALID: int name_compare = h1.host_name().compare(h2.host_name());
return l10n_util::GetStringUTF8(IDS_ERROR_NETWORK_ERROR); if (name_compare != 0) {
default: return name_compare < 0;
return l10n_util::GetStringFUTF8(IDS_SERVER_COMMUNICATION_ERROR,
base::NumberToString16(error_code));
} }
// Sort by last seen time if names are identical.
return h1.last_seen_time() < h2.last_seen_time();
} }
} // namespace } // namespace
...@@ -65,6 +65,24 @@ HostListService* HostListService::GetInstance() { ...@@ -65,6 +65,24 @@ HostListService* HostListService::GetInstance() {
} }
HostListService::HostListService() : weak_factory_(this) { HostListService::HostListService() : weak_factory_(this) {
token_getter_ =
ChromotingClientRuntime::GetInstance()->CreateOAuthTokenGetter();
directory_client_ = std::make_unique<DirectoryClient>(token_getter_.get());
Init();
}
HostListService::HostListService(
std::unique_ptr<DirectoryClient> directory_client)
: weak_factory_(this) {
directory_client_ = std::move(directory_client);
Init();
}
HostListService::~HostListService() {
[NSNotificationCenter.defaultCenter removeObserver:user_update_observer_];
}
void HostListService::Init() {
auto weak_this = weak_factory_.GetWeakPtr(); auto weak_this = weak_factory_.GetWeakPtr();
user_update_observer_ = [NSNotificationCenter.defaultCenter user_update_observer_ = [NSNotificationCenter.defaultCenter
addObserverForName:kUserDidUpdate addObserverForName:kUserDidUpdate
...@@ -78,10 +96,6 @@ HostListService::HostListService() : weak_factory_(this) { ...@@ -78,10 +96,6 @@ HostListService::HostListService() : weak_factory_(this) {
}]; }];
} }
HostListService::~HostListService() {
[NSNotificationCenter.defaultCenter removeObserver:user_update_observer_];
}
std::unique_ptr<HostListService::CallbackSubscription> std::unique_ptr<HostListService::CallbackSubscription>
HostListService::RegisterHostListStateCallback( HostListService::RegisterHostListStateCallback(
const base::RepeatingClosure& callback) { const base::RepeatingClosure& callback) {
...@@ -95,43 +109,12 @@ HostListService::RegisterFetchFailureCallback( ...@@ -95,43 +109,12 @@ HostListService::RegisterFetchFailureCallback(
} }
void HostListService::RequestFetch() { void HostListService::RequestFetch() {
auto weak_this = weak_factory_.GetWeakPtr(); if (state_ == State::FETCHING) {
[RemotingService.instance.authentication return;
callbackWithAccessToken:^(RemotingAuthenticationStatus status, }
NSString* userEmail, NSString* accessToken) { SetState(State::FETCHING);
if (status == RemotingAuthenticationStatusSuccess) { directory_client_->GetHostList(base::BindOnce(
if (weak_this) { &HostListService::HandleHostListResult, weak_factory_.GetWeakPtr()));
weak_this->StartHostListFetch(base::SysNSStringToUTF8(accessToken));
}
return;
}
FetchFailureReason failureReason;
switch (status) {
case RemotingAuthenticationStatusNetworkError:
failureReason = FetchFailureReason::NETWORK_ERROR;
break;
case RemotingAuthenticationStatusAuthError:
failureReason = FetchFailureReason::AUTH_ERROR;
break;
default:
NOTREACHED();
failureReason = FetchFailureReason::NETWORK_ERROR;
}
if (weak_this) {
weak_this->HandleFetchFailure(failureReason, 0);
}
}];
}
void HostListService::SetHostListFetcherForTesting(
std::unique_ptr<HostListFetcher> fetcher) {
host_list_fetcher_ = std::move(fetcher);
}
// static
std::unique_ptr<HostListService> HostListService::CreateInstanceForTesting() {
return std::make_unique<HostListService>();
} }
void HostListService::SetState(State state) { void HostListService::SetState(State state) {
...@@ -147,46 +130,32 @@ void HostListService::SetState(State state) { ...@@ -147,46 +130,32 @@ void HostListService::SetState(State state) {
host_list_state_callbacks_.Notify(); host_list_state_callbacks_.Notify();
} }
void HostListService::StartHostListFetch(const std::string& access_token) { void HostListService::HandleHostListResult(
if (state_ == State::FETCHING) { const grpc::Status& status,
const apis::v1::GetHostListResponse& response) {
if (!status.ok()) {
HandleFetchFailure(status);
return; return;
} }
SetState(State::FETCHING); hosts_.clear();
if (!host_list_fetcher_) { for (const auto& host : response.hosts()) {
host_list_fetcher_.reset(new HostListFetcher( hosts_.push_back(host);
ChromotingClientRuntime::GetInstance()->url_requester()));
} }
host_list_fetcher_->RetrieveHostlist( std::sort(hosts_.begin(), hosts_.end(), &CompareHost);
access_token, base::BindOnce(&HostListService::HandleHostListResult, SetState(State::FETCHED);
weak_factory_.GetWeakPtr()));
} }
void HostListService::HandleHostListResult( void HostListService::HandleFetchFailure(const grpc::Status& status) {
int responseCode, SetState(State::NOT_FETCHED);
const std::vector<remoting::HostInfo>& hostlist) {
if (responseCode == net::HTTP_OK) {
hosts_ = hostlist;
SetState(State::FETCHED);
return;
}
if (responseCode != HostListFetcher::RESPONSE_CODE_CANCELLED) { if (status.error_code() == grpc::StatusCode::CANCELLED) {
if (responseCode == net::HTTP_UNAUTHORIZED) { return;
[RemotingService.instance.authentication logout];
} else {
HandleFetchFailure(FetchFailureReason::REQUEST_ERROR, responseCode);
}
} }
SetState(State::NOT_FETCHED);
}
void HostListService::HandleFetchFailure(FetchFailureReason reason,
int error_code) {
last_fetch_failure_ = std::make_unique<FetchFailureInfo>(); last_fetch_failure_ = std::make_unique<FetchFailureInfo>();
last_fetch_failure_->reason = reason; last_fetch_failure_->reason = MapError(status.error_code());
last_fetch_failure_->error_code = error_code;
switch (reason) { switch (last_fetch_failure_->reason) {
case FetchFailureReason::NETWORK_ERROR: case FetchFailureReason::NETWORK_ERROR:
last_fetch_failure_->localized_description = last_fetch_failure_->localized_description =
l10n_util::GetStringUTF8(IDS_ERROR_NETWORK_ERROR); l10n_util::GetStringUTF8(IDS_ERROR_NETWORK_ERROR);
...@@ -195,24 +164,20 @@ void HostListService::HandleFetchFailure(FetchFailureReason reason, ...@@ -195,24 +164,20 @@ void HostListService::HandleFetchFailure(FetchFailureReason reason,
last_fetch_failure_->localized_description = last_fetch_failure_->localized_description =
l10n_util::GetStringUTF8(IDS_ERROR_OAUTH_TOKEN_INVALID); l10n_util::GetStringUTF8(IDS_ERROR_OAUTH_TOKEN_INVALID);
break; break;
case FetchFailureReason::REQUEST_ERROR:
last_fetch_failure_->localized_description =
GetRequestErrorMessage(error_code);
break;
default: default:
NOTREACHED(); last_fetch_failure_->localized_description = status.error_message();
} }
LOG(WARNING) << "Failed to fetch host list: " LOG(WARNING) << "Failed to fetch host list: "
<< last_fetch_failure_->localized_description << last_fetch_failure_->localized_description
<< " reason: " << static_cast<int>(reason) << " reason: " << static_cast<int>(last_fetch_failure_->reason);
<< ", error_code: " << error_code;
fetch_failure_callbacks_.Notify(); fetch_failure_callbacks_.Notify();
if (last_fetch_failure_->reason == FetchFailureReason::AUTH_ERROR) {
[RemotingService.instance.authentication logout];
}
} }
void HostListService::OnUserUpdated(bool is_user_signed_in) { void HostListService::OnUserUpdated(bool is_user_signed_in) {
if (host_list_fetcher_) { directory_client_->CancelPendingRequests();
host_list_fetcher_->CancelFetch();
}
SetState(State::NOT_FETCHED); SetState(State::NOT_FETCHED);
if (is_user_signed_in) { if (is_user_signed_in) {
RequestFetch(); RequestFetch();
......
...@@ -13,8 +13,6 @@ ...@@ -13,8 +13,6 @@
#import "base/bind.h" #import "base/bind.h"
#import "ios/third_party/material_components_ios/src/components/Snackbar/src/MaterialSnackbar.h" #import "ios/third_party/material_components_ios/src/components/Snackbar/src/MaterialSnackbar.h"
#import "remoting/ios/facade/host_info.h"
#import "remoting/ios/facade/host_list_fetcher.h"
#import "remoting/ios/facade/ios_client_runtime_delegate.h" #import "remoting/ios/facade/ios_client_runtime_delegate.h"
#import "remoting/ios/facade/remoting_service.h" #import "remoting/ios/facade/remoting_service.h"
#import "remoting/ios/persistence/remoting_keychain.h" #import "remoting/ios/persistence/remoting_keychain.h"
......
...@@ -104,7 +104,6 @@ static void ResolveFeedbackDataCallback( ...@@ -104,7 +104,6 @@ static void ResolveFeedbackDataCallback(
entryPoint:(remoting::ChromotingEvent::SessionEntryPoint)entryPoint { entryPoint:(remoting::ChromotingEvent::SessionEntryPoint)entryPoint {
DCHECK(_runtime->ui_task_runner()->BelongsToCurrentThread()); DCHECK(_runtime->ui_task_runner()->BelongsToCurrentThread());
DCHECK(hostInfo); DCHECK(hostInfo);
DCHECK(hostInfo.jabberId);
DCHECK(hostInfo.hostId); DCHECK(hostInfo.hostId);
DCHECK(hostInfo.publicKey); DCHECK(hostInfo.publicKey);
...@@ -114,6 +113,7 @@ static void ResolveFeedbackDataCallback( ...@@ -114,6 +113,7 @@ static void ResolveFeedbackDataCallback(
info.username = base::SysNSStringToUTF8(username); info.username = base::SysNSStringToUTF8(username);
info.auth_token = base::SysNSStringToUTF8(accessToken); info.auth_token = base::SysNSStringToUTF8(accessToken);
info.host_jid = base::SysNSStringToUTF8(hostInfo.jabberId); info.host_jid = base::SysNSStringToUTF8(hostInfo.jabberId);
info.host_ftl_id = base::SysNSStringToUTF8(hostInfo.ftlId);
info.host_id = base::SysNSStringToUTF8(hostInfo.hostId); info.host_id = base::SysNSStringToUTF8(hostInfo.hostId);
info.host_pubkey = base::SysNSStringToUTF8(hostInfo.publicKey); info.host_pubkey = base::SysNSStringToUTF8(hostInfo.publicKey);
info.host_os = base::SysNSStringToUTF8(hostInfo.hostOs); info.host_os = base::SysNSStringToUTF8(hostInfo.hostOs);
......
...@@ -3,15 +3,24 @@ ...@@ -3,15 +3,24 @@
# found in the LICENSE file. # found in the LICENSE file.
import("//third_party/grpc/grpc_library.gni") import("//third_party/grpc/grpc_library.gni")
import("//third_party/protobuf/proto_library.gni")
cc_grpc_library("directory_grpc_library") { proto_library("directory_proto") {
sources = [ sources = [
"directory_messages.proto", "directory_messages.proto",
"directory_service.proto",
"host_info.proto", "host_info.proto",
] ]
} }
cc_grpc_library("directory_grpc_library") {
sources = [
"directory_service.proto",
]
deps = [
":directory_proto",
]
}
if (is_android) { if (is_android) {
import("///build/config/android/rules.gni") import("///build/config/android/rules.gni")
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment