Commit 5f0fdf8b authored by fgorski@chromium.org's avatar fgorski@chromium.org

[GCM] Unregistration request

Wrapper over URL fetcher allowing for making request to remove registration of apps from the GCM. It pretends to be an API call instead of GCM client infrastructure code on purpose, to omit certain server side logic.

Patch includes:
* Implementation of the request using a URL fetcher with delete request
* Parsing of the response, with error handling
* triggering of the retry logic based on the provided backoff policy and when the last error allows to retry
* UMA logging of the request result.

BUG=284553

Review URL: https://codereview.chromium.org/147763008

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@251496 0039d316-1c4b-4281-b951-d872f2087c98
parent 5429cf40
// Copyright 2014 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 "google_apis/gcm/engine/unregistration_request.h"
#include "base/bind.h"
#include "base/message_loop/message_loop.h"
#include "base/metrics/histogram.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_piece.h"
#include "base/values.h"
#include "net/base/escape.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_status_code.h"
#include "net/url_request/url_fetcher.h"
#include "net/url_request/url_request_context_getter.h"
#include "net/url_request/url_request_status.h"
#include "url/gurl.h"
namespace gcm {
namespace {
const char kRegistrationURL[] =
"https://android.clients.google.com/c2dm/register3";
const char kRequestContentType[] = "application/x-www-form-urlencoded";
// Request constants.
const char kAppIdKey[] = "app";
const char kDeleteKey[] = "delete";
const char kDeleteValue[] = "true";
const char kDeviceIdKey[] = "device";
const char kLoginHeader[] = "AidLogin";
const char kUnregistrationCallerKey[] = "gcm_unreg_caller";
// We are going to set the value to "false" in order to forcefully unregister
// the application.
const char kUnregistrationCallerValue[] = "false";
// Response constants.
const char kDeletedPrefix[] = "deleted=";
const char kErrorPrefix[] = "Error=";
const char kInvalidParameters[] = "INVALID_PARAMETERS";
// Outcome of the response parsing. Note that these enums are consumed by a
// histogram, so ordering should not be modified.
enum UnregistrationRequestStatus {
SUCCESS, // Unregistration completed successfully.
URL_FETCHING_FAILED, // URL fetching failed.
NO_RESPONSE_BODY, // No response body.
RESPONSE_PARSING_FAILED, // Failed to parse a meaningful output from response
// body.
INCORRECT_APP_ID, // App ID returned by the fetcher does not match
// request.
INVALID_PARAMETERS, // Request parameters were invalid.
SERVICE_UNAVAILABLE, // Unregistration service unavailable.
INTERNAL_SERVER_ERROR, // Internal server error happened during request.
HTTP_NOT_OK, // HTTP response code was not OK.
UNKNOWN_ERROR, // Unknown error.
// NOTE: Always keep this entry at the end. Add new status types only
// immediately above this line. Make sure to update the corresponding
// histogram enum accordingly.
UNREGISTRATION_STATUS_COUNT,
};
void BuildFormEncoding(const std::string& key,
const std::string& value,
std::string* out) {
if (!out->empty())
out->append("&");
out->append(key + "=" + net::EscapeUrlEncodedData(value, true));
}
UnregistrationRequestStatus ParseFetcherResponse(const net::URLFetcher* source,
std::string request_app_id) {
if (!source->GetStatus().is_success()) {
DVLOG(1) << "Fetcher failed";
return URL_FETCHING_FAILED;
}
net::HttpStatusCode response_status = static_cast<net::HttpStatusCode>(
source->GetResponseCode());
if (response_status != net::HTTP_OK) {
DVLOG(1) << "HTTP Status code is not OK, but: " << response_status;
if (response_status == net::HTTP_SERVICE_UNAVAILABLE)
return SERVICE_UNAVAILABLE;
else if (response_status == net::HTTP_INTERNAL_SERVER_ERROR)
return INTERNAL_SERVER_ERROR;
return HTTP_NOT_OK;
}
std::string response;
if (!source->GetResponseAsString(&response)) {
DVLOG(1) << "Failed to get response body.";
return NO_RESPONSE_BODY;
}
DVLOG(1) << "Parsing unregistration response.";
if (response.find(kDeletedPrefix) != std::string::npos) {
std::string app_id = response.substr(
response.find(kDeletedPrefix) + arraysize(kDeletedPrefix) - 1);
if (app_id == request_app_id)
return SUCCESS;
return INCORRECT_APP_ID;
}
if (response.find(kErrorPrefix) != std::string::npos) {
std::string error = response.substr(
response.find(kErrorPrefix) + arraysize(kErrorPrefix) - 1);
if (error == kInvalidParameters)
return INVALID_PARAMETERS;
return UNKNOWN_ERROR;
}
DVLOG(1) << "Not able to parse a meaningful output from response body."
<< response;
return RESPONSE_PARSING_FAILED;
}
} // namespace
UnregistrationRequest::RequestInfo::RequestInfo(
uint64 android_id,
uint64 security_token,
const std::string& app_id)
: android_id(android_id),
security_token(security_token),
app_id(app_id) {
}
UnregistrationRequest::RequestInfo::~RequestInfo() {}
UnregistrationRequest::UnregistrationRequest(
const RequestInfo& request_info,
const net::BackoffEntry::Policy& backoff_policy,
const UnregistrationCallback& callback,
scoped_refptr<net::URLRequestContextGetter> request_context_getter)
: callback_(callback),
request_info_(request_info),
backoff_entry_(&backoff_policy),
request_context_getter_(request_context_getter),
weak_ptr_factory_(this) {
}
UnregistrationRequest::~UnregistrationRequest() {}
void UnregistrationRequest::Start() {
DCHECK(!callback_.is_null());
DCHECK(request_info_.android_id != 0UL);
DCHECK(request_info_.security_token != 0UL);
DCHECK(!url_fetcher_.get());
url_fetcher_.reset(net::URLFetcher::Create(
GURL(kRegistrationURL), net::URLFetcher::DELETE_REQUEST, this));
url_fetcher_->SetRequestContext(request_context_getter_);
std::string android_id = base::Uint64ToString(request_info_.android_id);
std::string auth_header =
std::string(kLoginHeader) + " " + android_id + ":" +
base::Uint64ToString(request_info_.security_token);
net::HttpRequestHeaders headers;
headers.SetHeader(net::HttpRequestHeaders::kAuthorization, auth_header);
headers.SetHeader(kAppIdKey, request_info_.app_id);
url_fetcher_->SetExtraRequestHeaders(headers.ToString());
std::string body;
BuildFormEncoding(kAppIdKey, request_info_.app_id, &body);
BuildFormEncoding(kDeviceIdKey, android_id, &body);
BuildFormEncoding(kDeleteKey, kDeleteValue, &body);
BuildFormEncoding(kUnregistrationCallerKey,
kUnregistrationCallerValue,
&body);
DVLOG(1) << "Unregistration request: " << body;
url_fetcher_->SetUploadData(kRequestContentType, body);
DVLOG(1) << "Performing unregistration for: " << request_info_.app_id;
url_fetcher_->Start();
}
void UnregistrationRequest::RetryWithBackoff(bool update_backoff) {
if (update_backoff) {
url_fetcher_.reset();
backoff_entry_.InformOfRequest(false);
}
if (backoff_entry_.ShouldRejectRequest()) {
DVLOG(1) << "Delaying GCM unregistration of app: "
<< request_info_.app_id << ", for "
<< backoff_entry_.GetTimeUntilRelease().InMilliseconds()
<< " milliseconds.";
base::MessageLoop::current()->PostDelayedTask(
FROM_HERE,
base::Bind(&UnregistrationRequest::RetryWithBackoff,
weak_ptr_factory_.GetWeakPtr(),
false),
backoff_entry_.GetTimeUntilRelease());
return;
}
Start();
}
void UnregistrationRequest::OnURLFetchComplete(const net::URLFetcher* source) {
UnregistrationRequestStatus status =
ParseFetcherResponse(source, request_info_.app_id);
DVLOG(1) << "UnregistrationRequestStauts: " << status;
UMA_HISTOGRAM_ENUMERATION("GCM.UnregistrationRequestStatus",
status,
UNREGISTRATION_STATUS_COUNT);
if (status == URL_FETCHING_FAILED ||
status == SERVICE_UNAVAILABLE ||
status == INTERNAL_SERVER_ERROR ||
status == INCORRECT_APP_ID ||
status == RESPONSE_PARSING_FAILED) {
RetryWithBackoff(true);
} else {
// status == SUCCESS || HTTP_NOT_OK || NO_RESPONSE_BODY ||
// INVALID_PARAMETERS || UNKNOWN_ERROR
callback_.Run(status == SUCCESS);
}
}
} // namespace gcm
// Copyright 2014 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 GOOGLE_APIS_GCM_ENGINE_UNREGISTRATION_REQUEST_H_
#define GOOGLE_APIS_GCM_ENGINE_UNREGISTRATION_REQUEST_H_
#include "base/basictypes.h"
#include "base/callback.h"
#include "base/memory/ref_counted.h"
#include "base/memory/scoped_ptr.h"
#include "base/memory/weak_ptr.h"
#include "google_apis/gcm/base/gcm_export.h"
#include "net/base/backoff_entry.h"
#include "net/url_request/url_fetcher_delegate.h"
namespace net {
class URLRequestContextGetter;
}
namespace gcm {
// Unregistration request is used to revoke registration IDs for applications
// that were uninstalled and should no longer receive GCM messages. In case an
// attempt to unregister fails, it will retry using the backoff policy.
// TODO(fgorski): Consider sharing code with RegistrationRequest if possible.
class GCM_EXPORT UnregistrationRequest : public net::URLFetcherDelegate {
public:
// Callback completing the unregistration request.
typedef base::Callback<void(bool success)> UnregistrationCallback;
// Details of the of the Unregistration Request. All parameters are mandatory.
struct GCM_EXPORT RequestInfo {
RequestInfo(uint64 android_id,
uint64 security_token,
const std::string& app_id);
~RequestInfo();
// Android ID of the device.
uint64 android_id;
// Security token of the device.
uint64 security_token;
// Application ID.
std::string app_id;
};
// Creates an instance of UnregistrationRequest. |callback| will be called
// once registration has been revoked or there has been an error that makes
// further retries pointless.
UnregistrationRequest(
const RequestInfo& request_info,
const net::BackoffEntry::Policy& backoff_policy,
const UnregistrationCallback& callback,
scoped_refptr<net::URLRequestContextGetter> request_context_getter);
virtual ~UnregistrationRequest();
// Starts an unregistration request.
void Start();
// URLFetcherDelegate implementation.
virtual void OnURLFetchComplete(const net::URLFetcher* source) OVERRIDE;
private:
// Schedules a retry attempt and informs the backoff of previous request's
// failure, when |update_backoff| is true.
void RetryWithBackoff(bool update_backoff);
UnregistrationCallback callback_;
RequestInfo request_info_;
net::BackoffEntry backoff_entry_;
scoped_refptr<net::URLRequestContextGetter> request_context_getter_;
scoped_ptr<net::URLFetcher> url_fetcher_;
base::WeakPtrFactory<UnregistrationRequest> weak_ptr_factory_;
DISALLOW_COPY_AND_ASSIGN(UnregistrationRequest);
};
} // namespace gcm
#endif // GOOGLE_APIS_GCM_ENGINE_UNREGISTRATION_REQUEST_H_
// Copyright 2014 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 <map>
#include <string>
#include <vector>
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_tokenizer.h"
#include "google_apis/gcm/engine/unregistration_request.h"
#include "net/url_request/test_url_fetcher_factory.h"
#include "net/url_request/url_request_test_util.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace gcm {
namespace {
const uint64 kAndroidId = 42UL;
const char kLoginHeader[] = "AidLogin";
const char kAppId[] = "TestAppId";
const char kDeletedAppId[] = "deleted=TestAppId";
const uint64 kSecurityToken = 77UL;
// Backoff policy for testing registration request.
const net::BackoffEntry::Policy kDefaultBackoffPolicy = {
// Number of initial errors (in sequence) to ignore before applying
// exponential back-off rules.
// Explicitly set to 2 to skip the delay on the first retry, as we are not
// trying to test the backoff itself, but rather the fact that retry happens.
1,
// Initial delay for exponential back-off in ms.
15000, // 15 seconds.
// Factor by which the waiting time will be multiplied.
2,
// Fuzzing percentage. ex: 10% will spread requests randomly
// between 90%-100% of the calculated time.
0.5, // 50%.
// Maximum amount of time we are willing to delay our request in ms.
1000 * 60 * 5, // 5 minutes.
// Time to keep an entry from being discarded even when it
// has no significant state, -1 to never discard.
-1,
// Don't use initial delay unless the last request was an error.
false,
};
} // namespace
class UnregistrationRequestTest : public testing::Test {
public:
UnregistrationRequestTest();
virtual ~UnregistrationRequestTest();
void UnregistrationCallback(bool success);
void CreateRequest();
void SetResponseStatusAndString(net::HttpStatusCode status_code,
const std::string& response_body);
void CompleteFetch();
protected:
bool callback_called_;
bool unregistration_successful_;
scoped_ptr<UnregistrationRequest> request_;
base::MessageLoop message_loop_;
net::TestURLFetcherFactory url_fetcher_factory_;
scoped_refptr<net::TestURLRequestContextGetter> url_request_context_getter_;
};
UnregistrationRequestTest::UnregistrationRequestTest()
: callback_called_(false),
unregistration_successful_(false),
url_request_context_getter_(new net::TestURLRequestContextGetter(
message_loop_.message_loop_proxy())) {}
UnregistrationRequestTest::~UnregistrationRequestTest() {}
void UnregistrationRequestTest::UnregistrationCallback(bool success) {
callback_called_ = true;
unregistration_successful_ = success;
}
void UnregistrationRequestTest::CreateRequest() {
request_.reset(new UnregistrationRequest(
UnregistrationRequest::RequestInfo(kAndroidId,
kSecurityToken,
kAppId),
kDefaultBackoffPolicy,
base::Bind(&UnregistrationRequestTest::UnregistrationCallback,
base::Unretained(this)),
url_request_context_getter_.get()));
}
void UnregistrationRequestTest::SetResponseStatusAndString(
net::HttpStatusCode status_code,
const std::string& response_body) {
net::TestURLFetcher* fetcher = url_fetcher_factory_.GetFetcherByID(0);
ASSERT_TRUE(fetcher);
fetcher->set_response_code(status_code);
fetcher->SetResponseString(response_body);
}
void UnregistrationRequestTest::CompleteFetch() {
unregistration_successful_ = false;
callback_called_ = false;
net::TestURLFetcher* fetcher = url_fetcher_factory_.GetFetcherByID(0);
ASSERT_TRUE(fetcher);
fetcher->delegate()->OnURLFetchComplete(fetcher);
}
TEST_F(UnregistrationRequestTest, RequestDataPassedToFetcher) {
CreateRequest();
request_->Start();
// Get data sent by request.
net::TestURLFetcher* fetcher = url_fetcher_factory_.GetFetcherByID(0);
ASSERT_TRUE(fetcher);
// Verify that authorization header was put together properly.
net::HttpRequestHeaders headers;
fetcher->GetExtraRequestHeaders(&headers);
std::string auth_header;
headers.GetHeader(net::HttpRequestHeaders::kAuthorization, &auth_header);
base::StringTokenizer auth_tokenizer(auth_header, " :");
ASSERT_TRUE(auth_tokenizer.GetNext());
EXPECT_EQ(kLoginHeader, auth_tokenizer.token());
ASSERT_TRUE(auth_tokenizer.GetNext());
EXPECT_EQ(base::Uint64ToString(kAndroidId), auth_tokenizer.token());
ASSERT_TRUE(auth_tokenizer.GetNext());
EXPECT_EQ(base::Uint64ToString(kSecurityToken), auth_tokenizer.token());
std::string app_id_header;
headers.GetHeader("app", &app_id_header);
EXPECT_EQ(kAppId, app_id_header);
std::map<std::string, std::string> expected_pairs;
expected_pairs["app"] = kAppId;
expected_pairs["device"] = base::Uint64ToString(kAndroidId);
expected_pairs["delete"] = "true";
expected_pairs["gcm_unreg_caller"] = "false";
// Verify data was formatted properly.
std::string upload_data = fetcher->upload_data();
base::StringTokenizer data_tokenizer(upload_data, "&=");
while (data_tokenizer.GetNext()) {
std::map<std::string, std::string>::iterator iter =
expected_pairs.find(data_tokenizer.token());
ASSERT_TRUE(iter != expected_pairs.end()) << data_tokenizer.token();
ASSERT_TRUE(data_tokenizer.GetNext()) << data_tokenizer.token();
EXPECT_EQ(iter->second, data_tokenizer.token());
// Ensure that none of the keys appears twice.
expected_pairs.erase(iter);
}
EXPECT_EQ(0UL, expected_pairs.size());
}
TEST_F(UnregistrationRequestTest, SuccessfulUnregistration) {
CreateRequest();
request_->Start();
SetResponseStatusAndString(net::HTTP_OK, kDeletedAppId);
CompleteFetch();
EXPECT_TRUE(callback_called_);
EXPECT_TRUE(unregistration_successful_);
}
TEST_F(UnregistrationRequestTest, ResponseHttpStatusNotOK) {
CreateRequest();
request_->Start();
SetResponseStatusAndString(net::HTTP_UNAUTHORIZED, "");
CompleteFetch();
EXPECT_TRUE(callback_called_);
EXPECT_FALSE(unregistration_successful_);
}
TEST_F(UnregistrationRequestTest, ResponseEmpty) {
CreateRequest();
request_->Start();
SetResponseStatusAndString(net::HTTP_OK, "");
CompleteFetch();
EXPECT_FALSE(callback_called_);
EXPECT_FALSE(unregistration_successful_);
SetResponseStatusAndString(net::HTTP_OK, kDeletedAppId);
CompleteFetch();
EXPECT_TRUE(callback_called_);
EXPECT_TRUE(unregistration_successful_);
}
TEST_F(UnregistrationRequestTest, InvalidParametersError) {
CreateRequest();
request_->Start();
SetResponseStatusAndString(net::HTTP_OK, "Error=INVALID_PARAMETERS");
CompleteFetch();
EXPECT_TRUE(callback_called_);
EXPECT_FALSE(unregistration_successful_);
}
TEST_F(UnregistrationRequestTest, UnkwnownError) {
CreateRequest();
request_->Start();
SetResponseStatusAndString(net::HTTP_OK, "Error=XXX");
CompleteFetch();
EXPECT_TRUE(callback_called_);
EXPECT_FALSE(unregistration_successful_);
}
TEST_F(UnregistrationRequestTest, ServiceUnavailable) {
CreateRequest();
request_->Start();
SetResponseStatusAndString(net::HTTP_SERVICE_UNAVAILABLE, "");
CompleteFetch();
EXPECT_FALSE(callback_called_);
SetResponseStatusAndString(net::HTTP_OK, kDeletedAppId);
CompleteFetch();
EXPECT_TRUE(callback_called_);
EXPECT_TRUE(unregistration_successful_);
}
TEST_F(UnregistrationRequestTest, InternalServerError) {
CreateRequest();
request_->Start();
SetResponseStatusAndString(net::HTTP_INTERNAL_SERVER_ERROR, "");
CompleteFetch();
EXPECT_FALSE(callback_called_);
SetResponseStatusAndString(net::HTTP_OK, kDeletedAppId);
CompleteFetch();
EXPECT_TRUE(callback_called_);
EXPECT_TRUE(unregistration_successful_);
}
TEST_F(UnregistrationRequestTest, IncorrectAppId) {
CreateRequest();
request_->Start();
SetResponseStatusAndString(net::HTTP_OK, "deleted=OtherTestAppId");
CompleteFetch();
EXPECT_FALSE(callback_called_);
SetResponseStatusAndString(net::HTTP_OK, kDeletedAppId);
CompleteFetch();
EXPECT_TRUE(callback_called_);
EXPECT_TRUE(unregistration_successful_);
}
TEST_F(UnregistrationRequestTest, ResponseParsingFailed) {
CreateRequest();
request_->Start();
SetResponseStatusAndString(net::HTTP_OK, "some malformed response");
CompleteFetch();
EXPECT_FALSE(callback_called_);
SetResponseStatusAndString(net::HTTP_OK, kDeletedAppId);
CompleteFetch();
EXPECT_TRUE(callback_called_);
EXPECT_TRUE(unregistration_successful_);
}
} // namespace gcm
...@@ -64,6 +64,8 @@ ...@@ -64,6 +64,8 @@
'engine/mcs_client.h', 'engine/mcs_client.h',
'engine/registration_request.cc', 'engine/registration_request.cc',
'engine/registration_request.h', 'engine/registration_request.h',
'engine/unregistration_request.cc',
'engine/unregistration_request.h',
'gcm_client.cc', 'gcm_client.cc',
'gcm_client.h', 'gcm_client.h',
'gcm_client_impl.cc', 'gcm_client_impl.cc',
...@@ -133,6 +135,7 @@ ...@@ -133,6 +135,7 @@
'engine/heartbeat_manager_unittest.cc', 'engine/heartbeat_manager_unittest.cc',
'engine/mcs_client_unittest.cc', 'engine/mcs_client_unittest.cc',
'engine/registration_request_unittest.cc', 'engine/registration_request_unittest.cc',
'engine/unregistration_request_unittest.cc',
'gcm_client_impl_unittest.cc' 'gcm_client_impl_unittest.cc'
] ]
}, },
......
...@@ -5606,6 +5606,11 @@ other types of suffix sets. ...@@ -5606,6 +5606,11 @@ other types of suffix sets.
</summary> </summary>
</histogram> </histogram>
<histogram name="GCM.UnregistrationRequestStatus"
enum="GCMUnregistrationRequestStatus">
<summary>Status code of the outcome of a GCM unregistration request.</summary>
</histogram>
<histogram name="GData.AuthSuccess" enum="GDataAuthResult"> <histogram name="GData.AuthSuccess" enum="GDataAuthResult">
<summary>Result of the authentication for Drive.</summary> <summary>Result of the authentication for Drive.</summary>
</histogram> </histogram>
...@@ -26573,6 +26578,19 @@ other types of suffix sets. ...@@ -26573,6 +26578,19 @@ other types of suffix sets.
<int value="5" label="Unknown error"/> <int value="5" label="Unknown error"/>
</enum> </enum>
<enum name="GCMUnregistrationRequestStatus" type="int">
<int value="0" label="Success"/>
<int value="1" label="URL fetching failed"/>
<int value="2" label="No response body"/>
<int value="3" label="Response parsing failed"/>
<int value="4" label="Incorrect App Id"/>
<int value="5" label="Invalid parameters"/>
<int value="6" label="Service unavailable"/>
<int value="7" label="Internal server error"/>
<int value="8" label="HTTP reponse code not OK"/>
<int value="9" label="Unknown error"/>
</enum>
<enum name="GDataAuthResult" type="int"> <enum name="GDataAuthResult" type="int">
<int value="0" label="FAILURE"/> <int value="0" label="FAILURE"/>
<int value="1" label="SUCCESS"/> <int value="1" label="SUCCESS"/>
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