Android Chromoting: Remove exit-fullscreen button.
[chromium-blink-merge.git] / components / copresence / rpc / rpc_handler.cc
blobc989531e3ff890cdec8efa9be536f97150c8256e
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"
7 #include "base/bind.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"
17 #if defined(OS_WIN)
18 #undef DeviceCapabilities
19 #endif
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";
46 namespace {
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);
61 return token;
65 // Logging
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(),
95 "Subscribe");
98 return result;
101 const std::string LoggingStrForToken(const std::string& auth_token) {
102 if (auth_token.empty())
103 return "anonymous";
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);
138 return state.Pass();
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);
150 return version;
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());
164 } // namespace
167 // Public functions.
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),
176 state_(state),
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),
183 kMaxInvalidTokens) {
184 DCHECK(delegate_);
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));
193 if (gcm_handler_) {
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_)
203 delete post;
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);
218 return;
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.
232 bool queue_request;
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.
239 } else {
240 queue_request = false;
243 // We're not registered, or registration is in progress.
244 if (queue_request) {
245 pending_requests_queue_.push_back(new PendingRequest(
246 request.Pass(), app_id, authenticated, status_callback));
247 return;
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,
262 device_id,
263 app_id,
264 authenticated,
265 request.Pass(),
266 // On destruction, this request will be cancelled.
267 base::Bind(&RpcHandler::ReportResponseHandler,
268 base::Unretained(this),
269 status_callback));
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)))
278 continue;
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,
291 bool authenticated,
292 const StatusCallback& callback)
293 : report(report.Pass()),
294 app_id(app_id),
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);
309 } else {
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";
324 Identity* identity =
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);
331 SendServerRequest(
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
337 authenticated,
338 request.Pass(),
339 base::Bind(&RpcHandler::RegisterResponseHandler,
340 // On destruction, this request will be cancelled.
341 base::Unretained(this),
342 authenticated,
343 gcm_pending));
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);
359 } else {
360 if (request->authenticated)
361 DCHECK(!auth_token_.empty());
362 SendReportRequest(request->report.Pass(),
363 request->app_id,
364 request->authenticated ? auth_token_ : std::string(),
365 request->callback);
367 delete request;
368 } else {
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";
388 return;
391 for (bool authenticated : auth_states) {
392 SendReportRequest(make_scoped_ptr(new ReportRequest(*request)),
393 std::string(),
394 authenticated ? auth_token_ : std::string(),
395 StatusCallback());
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) {
420 gcm_id_ = 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(
429 bool authenticated,
430 bool gcm_pending,
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: "
456 << device_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);
480 return;
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);
489 return;
492 if (ReportErrorLogged(response)) {
493 if (!status_callback.is_null())
494 status_callback.Run(FAIL);
495 return;
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()) {
519 case VALID:
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();
524 break;
525 case INVALID:
526 DVLOG(3) << "Discarding invalid token " << token.id();
527 invalid_audio_token_cache_.Add(token.id(), true);
528 break;
529 default:
530 DVLOG(2) << "Token " << token.id() << " has status code "
531 << token.status();
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()));
580 if (!app_id.empty())
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);
591 return header;
594 template <class T>
595 void RpcHandler::SendServerRequest(
596 const std::string& rpc_name,
597 const std::string& device_id,
598 const std::string& app_id,
599 bool authenticated,
600 scoped_ptr<T> request,
601 const PostCleanupCallback& response_handler) {
602 request->set_allocated_header(CreateRequestHeader(app_id, device_id));
603 if (authenticated)
604 DCHECK(!auth_token_.empty());
605 server_post_callback_.Run(delegate_->GetRequestContext(),
606 rpc_name,
607 delegate_->GetAPIKey(app_id),
608 authenticated ? auth_token_ : std::string(),
609 make_scoped_ptr<MessageLite>(request.release()),
610 response_handler);
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(
628 url_context_getter,
629 copresence_server_host,
630 rpc_name,
631 api_key,
632 auth_token,
633 command_line->GetSwitchValueASCII(switches::kCopresenceTracingToken),
634 *request_proto);
636 http_post->Start(base::Bind(callback, http_post));
637 pending_posts_.insert(http_post);
640 } // namespace copresence