// 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 "base/base64.h"
#include "base/i18n/time_formatting.h"
#include "base/metrics/histogram.h"
#include "base/sha1.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#if !defined(OS_ANDROID)
// channel_common.proto defines ANDROID constant that conflicts with Android
// build. At the same time TiclInvalidationService is not used on Android so it
// is safe to exclude these protos from Android build.
#include "google/cacheinvalidation/android_channel.pb.h"
#include "google/cacheinvalidation/channel_common.pb.h"
#include "google/cacheinvalidation/types.pb.h"
#endif
#include "components/invalidation/gcm_network_channel.h"
#include "components/invalidation/gcm_network_channel_delegate.h"
#include "google_apis/gaia/google_service_auth_error.h"
#include "net/http/http_status_code.h"
#include "net/url_request/url_fetcher.h"
#include "net/url_request/url_request_status.h"

namespace syncer {

namespace {

const char kCacheInvalidationEndpointUrl[] =
    "https://clients4.google.com/invalidation/android/request/";
const char kCacheInvalidationPackageName[] = "com.google.chrome.invalidations";

// Register backoff policy.
const net::BackoffEntry::Policy kRegisterBackoffPolicy = {
  // Number of initial errors (in sequence) to ignore before applying
  // exponential back-off rules.
  0,

  // Initial delay for exponential back-off in ms.
  2000, // 2 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.2, // 20%.

  // Maximum amount of time we are willing to delay our request in ms.
  1000 * 3600 * 4, // 4 hours.

  // 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,
};

// Incoming message status values for UMA_HISTOGRAM.
enum IncomingMessageStatus {
  INCOMING_MESSAGE_SUCCESS,
  MESSAGE_EMPTY,     // GCM message's content is missing or empty.
  INVALID_ENCODING,  // Base64Decode failed.
  INVALID_PROTO,     // Parsing protobuf failed.

  // This enum is used in UMA_HISTOGRAM_ENUMERATION. Insert new values above
  // this line.
  INCOMING_MESSAGE_STATUS_COUNT
};

// Outgoing message status values for UMA_HISTOGRAM.
enum OutgoingMessageStatus {
  OUTGOING_MESSAGE_SUCCESS,
  MESSAGE_DISCARDED,     // New message started before old one was sent.
  ACCESS_TOKEN_FAILURE,  // Requeting access token failed.
  POST_FAILURE,          // HTTP Post failed.

  // This enum is used in UMA_HISTOGRAM_ENUMERATION. Insert new values above
  // this line.
  OUTGOING_MESSAGE_STATUS_COUNT
};

const char kIncomingMessageStatusHistogram[] =
    "GCMInvalidations.IncomingMessageStatus";
const char kOutgoingMessageStatusHistogram[] =
    "GCMInvalidations.OutgoingMessageStatus";

void RecordIncomingMessageStatus(IncomingMessageStatus status) {
  UMA_HISTOGRAM_ENUMERATION(kIncomingMessageStatusHistogram,
                            status,
                            INCOMING_MESSAGE_STATUS_COUNT);
}

void RecordOutgoingMessageStatus(OutgoingMessageStatus status) {
  UMA_HISTOGRAM_ENUMERATION(kOutgoingMessageStatusHistogram,
                            status,
                            OUTGOING_MESSAGE_STATUS_COUNT);
}

}  // namespace

GCMNetworkChannel::GCMNetworkChannel(
    scoped_refptr<net::URLRequestContextGetter> request_context_getter,
    scoped_ptr<GCMNetworkChannelDelegate> delegate)
    : request_context_getter_(request_context_getter),
      delegate_(delegate.Pass()),
      register_backoff_entry_(new net::BackoffEntry(&kRegisterBackoffPolicy)),
      gcm_channel_online_(false),
      http_channel_online_(false),
      diagnostic_info_(this),
      weak_factory_(this) {
  net::NetworkChangeNotifier::AddNetworkChangeObserver(this);
  delegate_->Initialize(base::Bind(&GCMNetworkChannel::OnConnectionStateChanged,
                                   weak_factory_.GetWeakPtr()));
  Register();
}

GCMNetworkChannel::~GCMNetworkChannel() {
  net::NetworkChangeNotifier::RemoveNetworkChangeObserver(this);
}

void GCMNetworkChannel::Register() {
  delegate_->Register(base::Bind(&GCMNetworkChannel::OnRegisterComplete,
                                 weak_factory_.GetWeakPtr()));
}

void GCMNetworkChannel::OnRegisterComplete(
    const std::string& registration_id,
    gcm::GCMClient::Result result) {
  DCHECK(CalledOnValidThread());
  if (result == gcm::GCMClient::SUCCESS) {
    DCHECK(!registration_id.empty());
    DVLOG(2) << "Got registration_id";
    register_backoff_entry_->Reset();
    registration_id_ = registration_id;
    if (!cached_message_.empty())
      RequestAccessToken();
  } else {
    DVLOG(2) << "Register failed: " << result;
    // Retry in case of transient error.
    switch (result) {
      case gcm::GCMClient::NETWORK_ERROR:
      case gcm::GCMClient::SERVER_ERROR:
      case gcm::GCMClient::TTL_EXCEEDED:
      case gcm::GCMClient::UNKNOWN_ERROR: {
        register_backoff_entry_->InformOfRequest(false);
        base::MessageLoop::current()->PostDelayedTask(
            FROM_HERE,
            base::Bind(&GCMNetworkChannel::Register,
                       weak_factory_.GetWeakPtr()),
            register_backoff_entry_->GetTimeUntilRelease());
        break;
      }
      default:
        break;
    }
  }
  diagnostic_info_.registration_id_ = registration_id_;
  diagnostic_info_.registration_result_ = result;
}

void GCMNetworkChannel::SendMessage(const std::string& message) {
  DCHECK(CalledOnValidThread());
  DCHECK(!message.empty());
  DVLOG(2) << "SendMessage";
  diagnostic_info_.sent_messages_count_++;
  if (!cached_message_.empty()) {
    RecordOutgoingMessageStatus(MESSAGE_DISCARDED);
  }
  cached_message_ = message;

  if (!registration_id_.empty()) {
    RequestAccessToken();
  }
}

void GCMNetworkChannel::RequestAccessToken() {
  DCHECK(CalledOnValidThread());
  delegate_->RequestToken(base::Bind(&GCMNetworkChannel::OnGetTokenComplete,
                                     weak_factory_.GetWeakPtr()));
}

void GCMNetworkChannel::OnGetTokenComplete(
    const GoogleServiceAuthError& error,
    const std::string& token) {
  DCHECK(CalledOnValidThread());
  if (cached_message_.empty()) {
    // Nothing to do.
    return;
  }

  if (error.state() != GoogleServiceAuthError::NONE) {
    // Requesting access token failed. Persistent errors will be reported by
    // token service. Just drop this request, cacheinvalidations will retry
    // sending message and at that time we'll retry requesting access token.
    DVLOG(1) << "RequestAccessToken failed: " << error.ToString();
    RecordOutgoingMessageStatus(ACCESS_TOKEN_FAILURE);
    // Message won't get sent. Notify that http channel doesn't work.
    UpdateHttpChannelState(false);
    cached_message_.clear();
    return;
  }
  DCHECK(!token.empty());
  // Save access token in case POST fails and we need to invalidate it.
  access_token_ = token;

  DVLOG(2) << "Got access token, sending message";
  fetcher_.reset(net::URLFetcher::Create(
      BuildUrl(registration_id_), net::URLFetcher::POST, this));
  fetcher_->SetRequestContext(request_context_getter_);
  const std::string auth_header("Authorization: Bearer " + access_token_);
  fetcher_->AddExtraRequestHeader(auth_header);
  if (!echo_token_.empty()) {
    const std::string echo_header("echo-token: " + echo_token_);
    fetcher_->AddExtraRequestHeader(echo_header);
  }
  fetcher_->SetUploadData("application/x-protobuffer", cached_message_);
  fetcher_->Start();
  // Clear message to prevent accidentally resending it in the future.
  cached_message_.clear();
}

void GCMNetworkChannel::OnURLFetchComplete(const net::URLFetcher* source) {
  DCHECK(CalledOnValidThread());
  DCHECK_EQ(fetcher_, source);
  // Free fetcher at the end of function.
  scoped_ptr<net::URLFetcher> fetcher = fetcher_.Pass();

  net::URLRequestStatus status = fetcher->GetStatus();
  diagnostic_info_.last_post_response_code_ =
      status.is_success() ? source->GetResponseCode() : status.error();

  if (status.is_success() &&
      fetcher->GetResponseCode() == net::HTTP_UNAUTHORIZED) {
    DVLOG(1) << "URLFetcher failure: HTTP_UNAUTHORIZED";
    delegate_->InvalidateToken(access_token_);
  }

  if (!status.is_success() ||
      (fetcher->GetResponseCode() != net::HTTP_OK &&
       fetcher->GetResponseCode() != net::HTTP_NO_CONTENT)) {
    DVLOG(1) << "URLFetcher failure";
    RecordOutgoingMessageStatus(POST_FAILURE);
    // POST failed. Notify that http channel doesn't work.
    UpdateHttpChannelState(false);
    return;
  }

  RecordOutgoingMessageStatus(OUTGOING_MESSAGE_SUCCESS);
  // Successfully sent message. Http channel works.
  UpdateHttpChannelState(true);
  DVLOG(2) << "URLFetcher success";
}

void GCMNetworkChannel::OnIncomingMessage(const std::string& message,
                                          const std::string& echo_token) {
#if !defined(OS_ANDROID)
  if (!echo_token.empty())
    echo_token_ = echo_token;
  diagnostic_info_.last_message_empty_echo_token_ = echo_token.empty();
  diagnostic_info_.last_message_received_time_ = base::Time::Now();

  if (message.empty()) {
    RecordIncomingMessageStatus(MESSAGE_EMPTY);
    return;
  }
  std::string data;
  if (!Base64DecodeURLSafe(message, &data)) {
    RecordIncomingMessageStatus(INVALID_ENCODING);
    return;
  }
  ipc::invalidation::AddressedAndroidMessage android_message;
  if (!android_message.ParseFromString(data) ||
      !android_message.has_message()) {
    RecordIncomingMessageStatus(INVALID_PROTO);
    return;
  }
  DVLOG(2) << "Deliver incoming message";
  RecordIncomingMessageStatus(INCOMING_MESSAGE_SUCCESS);
  UpdateGcmChannelState(true);
  DeliverIncomingMessage(android_message.message());
#else
  // This code shouldn't be invoked on Android.
  NOTREACHED();
#endif
}

void GCMNetworkChannel::OnConnectionStateChanged(bool online) {
  UpdateGcmChannelState(online);
}

void GCMNetworkChannel::OnNetworkChanged(
    net::NetworkChangeNotifier::ConnectionType connection_type) {
  // Network connection is restored. Let's notify cacheinvalidations so it has
  // chance to retry.
  NotifyNetworkStatusChange(
      connection_type != net::NetworkChangeNotifier::CONNECTION_NONE);
}

void GCMNetworkChannel::UpdateGcmChannelState(bool online) {
  if (gcm_channel_online_ == online)
    return;
  gcm_channel_online_ = online;
  InvalidatorState channel_state = TRANSIENT_INVALIDATION_ERROR;
  if (gcm_channel_online_ && http_channel_online_)
    channel_state = INVALIDATIONS_ENABLED;
  NotifyChannelStateChange(channel_state);
}

void GCMNetworkChannel::UpdateHttpChannelState(bool online) {
  if (http_channel_online_ == online)
    return;
  http_channel_online_ = online;
  InvalidatorState channel_state = TRANSIENT_INVALIDATION_ERROR;
  if (gcm_channel_online_ && http_channel_online_)
    channel_state = INVALIDATIONS_ENABLED;
  NotifyChannelStateChange(channel_state);
}

GURL GCMNetworkChannel::BuildUrl(const std::string& registration_id) {
  DCHECK(!registration_id.empty());

#if !defined(OS_ANDROID)
  ipc::invalidation::EndpointId endpoint_id;
  endpoint_id.set_c2dm_registration_id(registration_id);
  endpoint_id.set_client_key(std::string());
  endpoint_id.set_package_name(kCacheInvalidationPackageName);
  endpoint_id.mutable_channel_version()->set_major_version(
      ipc::invalidation::INITIAL);
  std::string endpoint_id_buffer;
  endpoint_id.SerializeToString(&endpoint_id_buffer);

  ipc::invalidation::NetworkEndpointId network_endpoint_id;
  network_endpoint_id.set_network_address(
      ipc::invalidation::NetworkEndpointId_NetworkAddress_ANDROID);
  network_endpoint_id.set_client_address(endpoint_id_buffer);
  std::string network_endpoint_id_buffer;
  network_endpoint_id.SerializeToString(&network_endpoint_id_buffer);

  std::string base64URLPiece;
  Base64EncodeURLSafe(network_endpoint_id_buffer, &base64URLPiece);

  std::string url(kCacheInvalidationEndpointUrl);
  url += base64URLPiece;
  return GURL(url);
#else
  // This code shouldn't be invoked on Android.
  NOTREACHED();
  return GURL();
#endif
}

void GCMNetworkChannel::Base64EncodeURLSafe(const std::string& input,
                                            std::string* output) {
  base::Base64Encode(input, output);
  // Covert to url safe alphabet.
  base::ReplaceChars(*output, "+", "-", output);
  base::ReplaceChars(*output, "/", "_", output);
  // Trim padding.
  size_t padding_size = 0;
  for (size_t i = output->size(); i > 0 && (*output)[i - 1] == '='; --i)
    ++padding_size;
  output->resize(output->size() - padding_size);
}

bool GCMNetworkChannel::Base64DecodeURLSafe(const std::string& input,
                                            std::string* output) {
  // Add padding.
  size_t padded_size = (input.size() + 3) - (input.size() + 3) % 4;
  std::string padded_input(input);
  padded_input.resize(padded_size, '=');
  // Convert to standard base64 alphabet.
  base::ReplaceChars(padded_input, "-", "+", &padded_input);
  base::ReplaceChars(padded_input, "_", "/", &padded_input);
  return base::Base64Decode(padded_input, output);
}

void GCMNetworkChannel::SetMessageReceiver(
    invalidation::MessageCallback* incoming_receiver) {
  delegate_->SetMessageReceiver(base::Bind(
      &GCMNetworkChannel::OnIncomingMessage, weak_factory_.GetWeakPtr()));
  SyncNetworkChannel::SetMessageReceiver(incoming_receiver);
}

void GCMNetworkChannel::RequestDetailedStatus(
    base::Callback<void(const base::DictionaryValue&)> callback) {
  callback.Run(*diagnostic_info_.CollectDebugData());
}

void GCMNetworkChannel::UpdateCredentials(const std::string& email,
                                          const std::string& token) {
  // Do nothing. We get access token by requesting it for every message.
}

int GCMNetworkChannel::GetInvalidationClientType() {
#if defined(OS_IOS)
  return ipc::invalidation::ClientType::CHROME_SYNC_GCM_IOS;
#else
  return ipc::invalidation::ClientType::CHROME_SYNC_GCM_DESKTOP;
#endif
}

void GCMNetworkChannel::ResetRegisterBackoffEntryForTest(
    const net::BackoffEntry::Policy* policy) {
  register_backoff_entry_.reset(new net::BackoffEntry(policy));
}

GCMNetworkChannelDiagnostic::GCMNetworkChannelDiagnostic(
    GCMNetworkChannel* parent)
    : parent_(parent),
      last_message_empty_echo_token_(false),
      last_post_response_code_(0),
      registration_result_(gcm::GCMClient::UNKNOWN_ERROR),
      sent_messages_count_(0) {}

scoped_ptr<base::DictionaryValue>
GCMNetworkChannelDiagnostic::CollectDebugData() const {
  scoped_ptr<base::DictionaryValue> status(new base::DictionaryValue);
  status->SetString("GCMNetworkChannel.Channel", "GCM");
  std::string reg_id_hash = base::SHA1HashString(registration_id_);
  status->SetString("GCMNetworkChannel.HashedRegistrationID",
                    base::HexEncode(reg_id_hash.c_str(), reg_id_hash.size()));
  status->SetString("GCMNetworkChannel.RegistrationResult",
                    GCMClientResultToString(registration_result_));
  status->SetBoolean("GCMNetworkChannel.HadLastMessageEmptyEchoToken",
                     last_message_empty_echo_token_);
  status->SetString(
      "GCMNetworkChannel.LastMessageReceivedTime",
      base::TimeFormatShortDateAndTime(last_message_received_time_));
  status->SetInteger("GCMNetworkChannel.LastPostResponseCode",
                     last_post_response_code_);
  status->SetInteger("GCMNetworkChannel.SentMessages", sent_messages_count_);
  status->SetInteger("GCMNetworkChannel.ReceivedMessages",
                     parent_->GetReceivedMessagesCount());
  return status.Pass();
}

std::string GCMNetworkChannelDiagnostic::GCMClientResultToString(
    const gcm::GCMClient::Result result) const {
#define ENUM_CASE(x) case x: return #x; break;
  switch (result) {
    ENUM_CASE(gcm::GCMClient::SUCCESS);
    ENUM_CASE(gcm::GCMClient::NETWORK_ERROR);
    ENUM_CASE(gcm::GCMClient::SERVER_ERROR);
    ENUM_CASE(gcm::GCMClient::TTL_EXCEEDED);
    ENUM_CASE(gcm::GCMClient::UNKNOWN_ERROR);
    ENUM_CASE(gcm::GCMClient::NOT_SIGNED_IN);
    ENUM_CASE(gcm::GCMClient::INVALID_PARAMETER);
    ENUM_CASE(gcm::GCMClient::ASYNC_OPERATION_PENDING);
    ENUM_CASE(gcm::GCMClient::GCM_DISABLED);
  }
  NOTREACHED();
  return "";
}

}  // namespace syncer