1 // Copyright 2014 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.
5 #include "components/copresence/rpc/rpc_handler.h"
8 #include "base/command_line.h"
9 #include "base/logging.h"
10 #include "base/strings/string_util.h"
11 #include "base/strings/stringprintf.h"
13 // TODO(ckehoe): time.h includes windows.h, which #defines DeviceCapabilities
14 // to DeviceCapabilitiesW. This breaks the pb.h headers below. For now,
15 // we fix this with an #undef.
16 #include "base/time/time.h"
18 #undef DeviceCapabilities
21 #include "components/audio_modem/public/audio_modem_types.h"
22 #include "components/copresence/copresence_state_impl.h"
23 #include "components/copresence/copresence_switches.h"
24 #include "components/copresence/handlers/directive_handler.h"
25 #include "components/copresence/handlers/gcm_handler.h"
26 #include "components/copresence/proto/codes.pb.h"
27 #include "components/copresence/proto/data.pb.h"
28 #include "components/copresence/proto/rpcs.pb.h"
29 #include "components/copresence/public/copresence_constants.h"
30 #include "components/copresence/public/copresence_delegate.h"
31 #include "components/copresence/rpc/http_post.h"
32 #include "net/http/http_status_code.h"
34 using google::protobuf::MessageLite
;
36 using audio_modem::AUDIBLE
;
37 using audio_modem::AudioToken
;
38 using audio_modem::INAUDIBLE
;
40 // TODO(ckehoe): Return error messages for bad requests.
42 namespace copresence
{
44 const char RpcHandler::kReportRequestRpcName
[] = "report";
48 const int kTokenLoggingSuffix
= 5;
49 const int kInvalidTokenExpiryTimeMs
= 10 * 60 * 1000; // 10 minutes.
50 const int kMaxInvalidTokens
= 10000;
51 const char kRegisterDeviceRpcName
[] = "registerdevice";
52 const char kDefaultCopresenceServer
[] =
53 "https://www.googleapis.com/copresence/v2/copresence";
55 // UrlSafe is defined as:
56 // '/' represented by a '_' and '+' represented by a '-'
57 // TODO(rkc): Move this to the wrapper.
58 std::string
ToUrlSafe(std::string token
) {
59 base::ReplaceChars(token
, "+", "-", &token
);
60 base::ReplaceChars(token
, "/", "_", &token
);
67 // Checks for a copresence error. If there is one, logs it and returns true.
68 bool IsErrorStatus(const Status
& status
) {
69 if (status
.code() != OK
) {
70 LOG(ERROR
) << "Copresence error code " << status
.code()
71 << (status
.message().empty() ? "" : ": " + status
.message());
73 return status
.code() != OK
;
76 void LogIfErrorStatus(const util::error::Code
& code
,
77 const std::string
& context
) {
78 LOG_IF(ERROR
, code
!= util::error::OK
)
79 << context
<< " error " << code
<< ". See "
80 << "cs/google3/util/task/codes.proto for more info.";
83 // If any errors occurred, logs them and returns true.
84 bool ReportErrorLogged(const ReportResponse
& response
) {
85 bool result
= IsErrorStatus(response
.header().status());
87 // The Report fails or succeeds as a unit. If any responses had errors,
88 // the header will too. Thus we don't need to propagate individual errors.
89 if (response
.has_update_signals_response())
90 LogIfErrorStatus(response
.update_signals_response().status(), "Update");
91 if (response
.has_manage_messages_response())
92 LogIfErrorStatus(response
.manage_messages_response().status(), "Publish");
93 if (response
.has_manage_subscriptions_response()) {
94 LogIfErrorStatus(response
.manage_subscriptions_response().status(),
101 const std::string
LoggingStrForToken(const std::string
& auth_token
) {
102 if (auth_token
.empty())
105 std::string token_suffix
= auth_token
.substr(
106 auth_token
.length() - kTokenLoggingSuffix
, kTokenLoggingSuffix
);
107 return base::StringPrintf("token ...%s", token_suffix
.c_str());
111 // Request construction
112 // TODO(ckehoe): Move these into a separate file?
114 template <typename T
>
115 BroadcastScanConfiguration
GetBroadcastScanConfig(const T
& msg
) {
116 if (msg
.has_token_exchange_strategy() &&
117 msg
.token_exchange_strategy().has_broadcast_scan_configuration()) {
118 return msg
.token_exchange_strategy().broadcast_scan_configuration();
120 return BROADCAST_SCAN_CONFIGURATION_UNKNOWN
;
123 scoped_ptr
<DeviceState
> GetDeviceCapabilities(const ReportRequest
& request
) {
124 scoped_ptr
<DeviceState
> state(new DeviceState
);
126 TokenTechnology
* ultrasound
=
127 state
->mutable_capabilities()->add_token_technology();
128 ultrasound
->set_medium(AUDIO_ULTRASOUND_PASSBAND
);
129 ultrasound
->add_instruction_type(TRANSMIT
);
130 ultrasound
->add_instruction_type(RECEIVE
);
132 TokenTechnology
* audible
=
133 state
->mutable_capabilities()->add_token_technology();
134 audible
->set_medium(AUDIO_AUDIBLE_DTMF
);
135 audible
->add_instruction_type(TRANSMIT
);
136 audible
->add_instruction_type(RECEIVE
);
141 // TODO(ckehoe): We're keeping this code in a separate function for now
142 // because we get a version string from Chrome, but the proto expects
143 // an int64 version. We should probably change the version proto
144 // to handle a more detailed version.
145 ClientVersion
* CreateVersion(const std::string
& client
,
146 const std::string
& version_name
) {
147 ClientVersion
* version
= new ClientVersion
;
148 version
->set_client(client
);
149 version
->set_version_name(version_name
);
153 void AddTokenToRequest(const AudioToken
& token
, ReportRequest
* request
) {
154 TokenObservation
* token_observation
=
155 request
->mutable_update_signals_request()->add_token_observation();
156 token_observation
->set_token_id(ToUrlSafe(token
.token
));
158 TokenSignals
* signals
= token_observation
->add_signals();
159 signals
->set_medium(token
.audible
? AUDIO_AUDIBLE_DTMF
160 : AUDIO_ULTRASOUND_PASSBAND
);
161 signals
->set_observed_time_millis(base::Time::Now().ToJsTime());
169 RpcHandler::RpcHandler(CopresenceDelegate
* delegate
,
170 CopresenceStateImpl
* state
,
171 DirectiveHandler
* directive_handler
,
172 GCMHandler
* gcm_handler
,
173 const MessagesCallback
& new_messages_callback
,
174 const PostCallback
& server_post_callback
)
175 : delegate_(delegate
),
177 directive_handler_(directive_handler
),
178 gcm_handler_(gcm_handler
),
179 new_messages_callback_(new_messages_callback
),
180 server_post_callback_(server_post_callback
),
181 invalid_audio_token_cache_(
182 base::TimeDelta::FromMilliseconds(kInvalidTokenExpiryTimeMs
),
185 DCHECK(directive_handler_
);
186 // |gcm_handler_| is optional.
188 if (server_post_callback_
.is_null()) {
189 server_post_callback_
=
190 base::Bind(&RpcHandler::SendHttpPost
, base::Unretained(this));
194 gcm_handler_
->GetGcmId(
195 base::Bind(&RpcHandler::RegisterGcmId
, base::Unretained(this)));
199 RpcHandler::~RpcHandler() {
200 // Do not use |directive_handler_| or |gcm_handler_| here.
201 // They will already have been destructed.
202 for (HttpPost
* post
: pending_posts_
)
206 void RpcHandler::SendReportRequest(scoped_ptr
<ReportRequest
> request
,
207 const std::string
& app_id
,
208 const std::string
& auth_token
,
209 const StatusCallback
& status_callback
) {
210 DCHECK(request
.get());
212 // Check that the app, if any, has some kind of authentication token.
213 // Don't allow it to piggyback on Chrome's credentials.
214 if (!app_id
.empty() && delegate_
->GetAPIKey(app_id
).empty() &&
215 auth_token
.empty()) {
216 LOG(ERROR
) << "App " << app_id
<< " has no API key or auth token";
217 status_callback
.Run(FAIL
);
221 // Store just one auth token since we should have only one account
222 // per instance of the copresence component.
223 // TODO(ckehoe): We may eventually need to support multiple auth tokens.
224 const bool authenticated
= !auth_token
.empty();
225 if (authenticated
&& auth_token
!= auth_token_
) {
226 LOG_IF(ERROR
, !auth_token_
.empty())
227 << "Overwriting old auth token: " << LoggingStrForToken(auth_token
);
228 auth_token_
= auth_token
;
231 // Check that we have a "device" registered for this authentication state.
233 const std::string device_id
= delegate_
->GetDeviceId(authenticated
);
234 if (device_id
.empty()) {
235 queue_request
= true;
236 if (pending_registrations_
.count(authenticated
) == 0)
237 RegisterDevice(authenticated
);
238 // else, registration is already in progress.
240 queue_request
= false;
243 // We're not registered, or registration is in progress.
245 pending_requests_queue_
.push_back(new PendingRequest(
246 request
.Pass(), app_id
, authenticated
, status_callback
));
250 DVLOG(3) << "Sending ReportRequest to server.";
252 // If we are unpublishing or unsubscribing, we need to stop those publish or
253 // subscribes right away, we don't need to wait for the server to tell us.
254 ProcessRemovedOperations(*request
);
256 request
->mutable_update_signals_request()->set_allocated_state(
257 GetDeviceCapabilities(*request
).release());
259 AddPlayingTokens(request
.get());
261 SendServerRequest(kReportRequestRpcName
,
266 // On destruction, this request will be cancelled.
267 base::Bind(&RpcHandler::ReportResponseHandler
,
268 base::Unretained(this),
272 void RpcHandler::ReportTokens(const std::vector
<AudioToken
>& tokens
) {
273 DCHECK(!tokens
.empty());
275 scoped_ptr
<ReportRequest
> request(new ReportRequest
);
276 for (const AudioToken
& token
: tokens
) {
277 if (invalid_audio_token_cache_
.HasKey(ToUrlSafe(token
.token
)))
279 DVLOG(3) << "Sending token " << token
.token
<< " to server";
280 AddTokenToRequest(token
, request
.get());
283 ReportOnAllDevices(request
.Pass());
287 // Private functions.
289 RpcHandler::PendingRequest::PendingRequest(scoped_ptr
<ReportRequest
> report
,
290 const std::string
& app_id
,
292 const StatusCallback
& callback
)
293 : report(report
.Pass()),
295 authenticated(authenticated
),
296 callback(callback
) {}
298 RpcHandler::PendingRequest::~PendingRequest() {}
300 void RpcHandler::RegisterDevice(bool authenticated
) {
301 DVLOG(2) << "Sending " << (authenticated
? "authenticated" : "anonymous")
302 << " registration to server.";
304 scoped_ptr
<RegisterDeviceRequest
> request(new RegisterDeviceRequest
);
306 // Add a GCM ID for authenticated registration, if we have one.
307 if (!authenticated
|| gcm_id_
.empty()) {
308 request
->mutable_push_service()->set_service(PUSH_SERVICE_NONE
);
310 DVLOG(2) << "Registering GCM ID with " << LoggingStrForToken(auth_token_
);
311 request
->mutable_push_service()->set_service(GCM
);
312 request
->mutable_push_service()->mutable_gcm_registration()
313 ->set_device_token(gcm_id_
);
316 // Only identify as a Chrome device if we're in anonymous mode.
317 // Authenticated calls come from a "GAIA device".
318 if (!authenticated
) {
319 // Make sure this isn't a duplicate anonymous registration.
320 // Duplicate authenticated registrations are allowed, to update the GCM ID.
321 DCHECK(delegate_
->GetDeviceId(false).empty())
322 << "Attempted anonymous re-registration";
325 request
->mutable_device_identifiers()->mutable_registrant();
326 identity
->set_type(CHROME
);
329 bool gcm_pending
= authenticated
&& gcm_handler_
&& gcm_id_
.empty();
330 pending_registrations_
.insert(authenticated
);
332 kRegisterDeviceRpcName
,
333 // The device is empty on first registration.
334 // When re-registering to pass on the GCM ID, it will be present.
335 delegate_
->GetDeviceId(authenticated
),
336 std::string(), // app ID
339 base::Bind(&RpcHandler::RegisterResponseHandler
,
340 // On destruction, this request will be cancelled.
341 base::Unretained(this),
346 void RpcHandler::ProcessQueuedRequests(bool authenticated
) {
347 // Track requests that are not in this auth state.
348 ScopedVector
<PendingRequest
> still_pending_requests
;
350 // If there is no device ID for this auth state, registration failed.
351 bool registration_failed
= delegate_
->GetDeviceId(authenticated
).empty();
353 // We momentarily take ownership of all the pointers in the queue.
354 // They are either deleted here or passed on to a new queue.
355 for (PendingRequest
* request
: pending_requests_queue_
) {
356 if (request
->authenticated
== authenticated
) {
357 if (registration_failed
) {
358 request
->callback
.Run(FAIL
);
360 if (request
->authenticated
)
361 DCHECK(!auth_token_
.empty());
362 SendReportRequest(request
->report
.Pass(),
364 request
->authenticated
? auth_token_
: std::string(),
369 // The request is in a different auth state.
370 still_pending_requests
.push_back(request
);
374 // Only keep the requests that weren't processed.
375 // All the pointers in the queue are now spoken for.
376 pending_requests_queue_
.weak_clear();
377 pending_requests_queue_
= still_pending_requests
.Pass();
380 void RpcHandler::ReportOnAllDevices(scoped_ptr
<ReportRequest
> request
) {
381 std::vector
<bool> auth_states
;
382 if (!auth_token_
.empty() && !delegate_
->GetDeviceId(true).empty())
383 auth_states
.push_back(true);
384 if (!delegate_
->GetDeviceId(false).empty())
385 auth_states
.push_back(false);
386 if (auth_states
.empty()) {
387 VLOG(2) << "Skipping reporting because no device IDs are registered";
391 for (bool authenticated
: auth_states
) {
392 SendReportRequest(make_scoped_ptr(new ReportRequest(*request
)),
394 authenticated
? auth_token_
: std::string(),
399 // Store a GCM ID and send it to the server if needed. The constructor passes
400 // this callback to the GCMHandler to receive the ID whenever it's ready.
401 // It may be returned immediately, if the ID is cached, or require a server
402 // round-trip. This ID must then be passed along to the copresence server.
403 // There are a few ways this can happen:
405 // 1. The GCM ID is available when we first register, and is passed along
406 // with the RegisterDeviceRequest.
408 // 2. The GCM ID becomes available after the RegisterDeviceRequest has
409 // completed. Then this function will invoke RegisterDevice()
410 // again to pass on the ID.
412 // 3. The GCM ID becomes available after the RegisterDeviceRequest is sent,
413 // but before it completes. In this case, the gcm_pending flag is passed
414 // through to the RegisterResponseHandler, which invokes RegisterDevice()
415 // again to pass on the ID. This function must skip pending registrations,
416 // as the device ID will be empty.
418 // TODO(ckehoe): Add tests for these scenarios.
419 void RpcHandler::RegisterGcmId(const std::string
& gcm_id
) {
421 if (!gcm_id
.empty()) {
422 const std::string
& device_id
= delegate_
->GetDeviceId(true);
423 if (!auth_token_
.empty() && !device_id
.empty())
424 RegisterDevice(true);
428 void RpcHandler::RegisterResponseHandler(
431 HttpPost
* completed_post
,
432 int http_status_code
,
433 const std::string
& response_data
) {
434 if (completed_post
) {
435 int elements_erased
= pending_posts_
.erase(completed_post
);
436 DCHECK_GT(elements_erased
, 0);
437 delete completed_post
;
440 int registrations_completed
= pending_registrations_
.erase(authenticated
);
441 DCHECK_GT(registrations_completed
, 0);
443 RegisterDeviceResponse response
;
444 const std::string token_str
=
445 LoggingStrForToken(authenticated
? auth_token_
: std::string());
446 if (http_status_code
!= net::HTTP_OK
) {
447 // TODO(ckehoe): Retry registration if appropriate.
448 LOG(ERROR
) << token_str
<< " device registration failed";
449 } else if (!response
.ParseFromString(response_data
)) {
450 LOG(ERROR
) << "Invalid RegisterDeviceResponse:\n" << response_data
;
451 } else if (!IsErrorStatus(response
.header().status())) {
452 const std::string
& device_id
= response
.registered_device_id();
453 DCHECK(!device_id
.empty());
454 delegate_
->SaveDeviceId(authenticated
, device_id
);
455 DVLOG(2) << token_str
<< " device registration successful. Id: "
458 // If we have a GCM ID now, and didn't before, pass it on to the server.
459 if (gcm_pending
&& !gcm_id_
.empty())
460 RegisterDevice(authenticated
);
463 // Send or fail requests on this auth token.
464 ProcessQueuedRequests(authenticated
);
467 void RpcHandler::ReportResponseHandler(const StatusCallback
& status_callback
,
468 HttpPost
* completed_post
,
469 int http_status_code
,
470 const std::string
& response_data
) {
471 if (completed_post
) {
472 int elements_erased
= pending_posts_
.erase(completed_post
);
473 DCHECK(elements_erased
);
474 delete completed_post
;
477 if (http_status_code
!= net::HTTP_OK
) {
478 if (!status_callback
.is_null())
479 status_callback
.Run(FAIL
);
483 DVLOG(3) << "Received ReportResponse.";
484 ReportResponse response
;
485 if (!response
.ParseFromString(response_data
)) {
486 LOG(ERROR
) << "Invalid ReportResponse";
487 if (!status_callback
.is_null())
488 status_callback
.Run(FAIL
);
492 if (ReportErrorLogged(response
)) {
493 if (!status_callback
.is_null())
494 status_callback
.Run(FAIL
);
498 for (const MessageResult
& result
:
499 response
.manage_messages_response().published_message_result()) {
500 DVLOG(2) << "Published message with id " << result
.published_message_id();
503 for (const SubscriptionResult
& result
:
504 response
.manage_subscriptions_response().subscription_result()) {
505 DVLOG(2) << "Created subscription with id " << result
.subscription_id();
508 if (response
.has_update_signals_response()) {
509 const UpdateSignalsResponse
& update_response
=
510 response
.update_signals_response();
511 new_messages_callback_
.Run(update_response
.message());
513 for (const Directive
& directive
: update_response
.directive())
514 directive_handler_
->AddDirective(directive
);
516 for (const Token
& token
: update_response
.token()) {
517 state_
->UpdateTokenStatus(token
.id(), token
.status());
518 switch (token
.status()) {
520 // TODO(rkc/ckehoe): Store the token in a |valid_token_cache_| with a
521 // short TTL (like 10s) and send it up with every report request.
522 // Then we'll still get messages while we're waiting to hear it again.
523 VLOG(1) << "Got valid token " << token
.id();
526 DVLOG(3) << "Discarding invalid token " << token
.id();
527 invalid_audio_token_cache_
.Add(token
.id(), true);
530 DVLOG(2) << "Token " << token
.id() << " has status code "
536 // TODO(ckehoe): Return a more detailed status response.
537 if (!status_callback
.is_null())
538 status_callback
.Run(SUCCESS
);
541 void RpcHandler::ProcessRemovedOperations(const ReportRequest
& request
) {
542 // Remove unpublishes.
543 if (request
.has_manage_messages_request()) {
544 for (const std::string
& unpublish
:
545 request
.manage_messages_request().id_to_unpublish()) {
546 directive_handler_
->RemoveDirectives(unpublish
);
550 // Remove unsubscribes.
551 if (request
.has_manage_subscriptions_request()) {
552 for (const std::string
& unsubscribe
:
553 request
.manage_subscriptions_request().id_to_unsubscribe()) {
554 directive_handler_
->RemoveDirectives(unsubscribe
);
559 void RpcHandler::AddPlayingTokens(ReportRequest
* request
) {
560 const std::string
& audible_token
=
561 directive_handler_
->GetCurrentAudioToken(AUDIBLE
);
562 const std::string
& inaudible_token
=
563 directive_handler_
->GetCurrentAudioToken(INAUDIBLE
);
565 if (!audible_token
.empty())
566 AddTokenToRequest(AudioToken(audible_token
, true), request
);
567 if (!inaudible_token
.empty())
568 AddTokenToRequest(AudioToken(inaudible_token
, false), request
);
571 // TODO(ckehoe): Pass in the version string and
572 // group this with the local functions up top.
573 RequestHeader
* RpcHandler::CreateRequestHeader(
574 const std::string
& app_id
,
575 const std::string
& device_id
) const {
576 RequestHeader
* header
= new RequestHeader
;
578 header
->set_allocated_framework_version(CreateVersion(
579 "Chrome", delegate_
->GetPlatformVersionString()));
581 header
->set_allocated_client_version(CreateVersion(app_id
, std::string()));
582 header
->set_current_time_millis(base::Time::Now().ToJsTime());
583 if (!device_id
.empty())
584 header
->set_registered_device_id(device_id
);
586 DeviceFingerprint
* fingerprint
= new DeviceFingerprint
;
587 fingerprint
->set_platform_version(delegate_
->GetPlatformVersionString());
588 fingerprint
->set_type(CHROME_PLATFORM_TYPE
);
589 header
->set_allocated_device_fingerprint(fingerprint
);
595 void RpcHandler::SendServerRequest(
596 const std::string
& rpc_name
,
597 const std::string
& device_id
,
598 const std::string
& app_id
,
600 scoped_ptr
<T
> request
,
601 const PostCleanupCallback
& response_handler
) {
602 request
->set_allocated_header(CreateRequestHeader(app_id
, device_id
));
604 DCHECK(!auth_token_
.empty());
605 server_post_callback_
.Run(delegate_
->GetRequestContext(),
607 delegate_
->GetAPIKey(app_id
),
608 authenticated
? auth_token_
: std::string(),
609 make_scoped_ptr
<MessageLite
>(request
.release()),
613 void RpcHandler::SendHttpPost(net::URLRequestContextGetter
* url_context_getter
,
614 const std::string
& rpc_name
,
615 const std::string
& api_key
,
616 const std::string
& auth_token
,
617 scoped_ptr
<MessageLite
> request_proto
,
618 const PostCleanupCallback
& callback
) {
619 // Create the base URL to call.
620 base::CommandLine
* command_line
= base::CommandLine::ForCurrentProcess();
621 const std::string copresence_server_host
=
622 command_line
->HasSwitch(switches::kCopresenceServer
) ?
623 command_line
->GetSwitchValueASCII(switches::kCopresenceServer
) :
624 kDefaultCopresenceServer
;
626 // Create the request and keep a pointer until it completes.
627 HttpPost
* http_post
= new HttpPost(
629 copresence_server_host
,
633 command_line
->GetSwitchValueASCII(switches::kCopresenceTracingToken
),
636 http_post
->Start(base::Bind(callback
, http_post
));
637 pending_posts_
.insert(http_post
);
640 } // namespace copresence