// Copyright (c) 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 "net/quic/quic_connectivity_probing_manager.h"

#include "base/bind.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_number_conversions.h"
#include "base/values.h"
#include "net/log/net_log_values.h"
#include "net/quic/address_utils.h"

namespace net {

namespace {

// Default to 2 seconds timeout as the maximum timeout.
const int64_t kMaxProbingTimeoutMs = 2000;

base::Value NetLogStartProbingParams(
    NetworkChangeNotifier::NetworkHandle network,
    const quic::QuicSocketAddress* peer_address,
    base::TimeDelta initial_timeout) {
  base::DictionaryValue dict;
  dict.SetKey("network", NetLogNumberValue(network));
  dict.SetString("peer address", peer_address->ToString());
  dict.SetKey("initial_timeout_ms",
              NetLogNumberValue(initial_timeout.InMilliseconds()));
  return std::move(dict);
}

base::Value NetLogProbeReceivedParams(
    NetworkChangeNotifier::NetworkHandle network,
    const IPEndPoint* self_address,
    const quic::QuicSocketAddress* peer_address) {
  base::DictionaryValue dict;
  dict.SetKey("network", NetLogNumberValue(network));
  dict.SetString("self address", self_address->ToString());
  dict.SetString("peer address", peer_address->ToString());
  return std::move(dict);
}

base::Value NetLogProbingDestinationParams(
    NetworkChangeNotifier::NetworkHandle network,
    const quic::QuicSocketAddress* peer_address) {
  base::DictionaryValue dict;
  dict.SetString("network", base::NumberToString(network));
  dict.SetString("peer address", peer_address->ToString());
  return std::move(dict);
}

}  // namespace

QuicConnectivityProbingManager::QuicConnectivityProbingManager(
    Delegate* delegate,
    base::SequencedTaskRunner* task_runner)
    : delegate_(delegate),
      is_running_(false),
      network_(NetworkChangeNotifier::kInvalidNetworkHandle),
      retry_count_(0),
      probe_start_time_(base::TimeTicks()),
      task_runner_(task_runner),
      last_self_address_(IPEndPoint()),
      stateless_reset_received_(false) {
  retransmit_timer_.SetTaskRunner(task_runner_);
}

QuicConnectivityProbingManager::~QuicConnectivityProbingManager() {
  CancelProbingIfAny();
}

int QuicConnectivityProbingManager::HandleWriteError(
    int error_code,
    scoped_refptr<QuicChromiumPacketWriter::ReusableIOBuffer> packet) {
  // Write error on the probing network is not recoverable.
  DVLOG(1) << "Probing packet encounters write error";
  // Post a task to notify |delegate_| that this probe failed and cancel
  // undergoing probing, which will delete the packet writer.
  task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(&QuicConnectivityProbingManager::NotifyDelegateProbeFailed,
                     weak_factory_.GetWeakPtr()));
  return error_code;
}

void QuicConnectivityProbingManager::OnWriteError(int error_code) {
  // Write error on the probing network.
  NotifyDelegateProbeFailed();
}

void QuicConnectivityProbingManager::OnWriteUnblocked() {}

void QuicConnectivityProbingManager::CancelProbing(
    NetworkChangeNotifier::NetworkHandle network,
    const quic::QuicSocketAddress& peer_address) {
  if (is_running_ && network == network_ && peer_address == peer_address_)
    CancelProbingIfAny();
}

void QuicConnectivityProbingManager::CancelProbingIfAny() {
  if (is_running_) {
    UMA_HISTOGRAM_BOOLEAN("Net.QuicSession.StatelessResetDuringProbing",
                          stateless_reset_received_);
    net_log_.AddEvent(
        NetLogEventType::QUIC_CONNECTIVITY_PROBING_MANAGER_CANCEL_PROBING, [&] {
          return NetLogProbingDestinationParams(network_, &peer_address_);
        });
  }
  is_running_ = false;
  if (socket_ != nullptr) {
    socket_->GetLocalAddress(&last_self_address_);
  }
  network_ = NetworkChangeNotifier::kInvalidNetworkHandle;
  stateless_reset_received_ = false;
  socket_.reset();
  writer_.reset();
  reader_.reset();
  retry_count_ = 0;
  probe_start_time_ = base::TimeTicks();
  initial_timeout_ = base::TimeDelta();
  retransmit_timer_.Stop();
}

void QuicConnectivityProbingManager::StartProbing(
    NetworkChangeNotifier::NetworkHandle network,
    const quic::QuicSocketAddress& peer_address,
    std::unique_ptr<DatagramClientSocket> socket,
    std::unique_ptr<QuicChromiumPacketWriter> writer,
    std::unique_ptr<QuicChromiumPacketReader> reader,
    base::TimeDelta initial_timeout,
    const NetLogWithSource& net_log) {
  DCHECK(peer_address != quic::QuicSocketAddress());

  if (IsUnderProbing(network, peer_address))
    return;

  // Start a new probe will always cancel the previous one.
  CancelProbingIfAny();

  is_running_ = true;
  network_ = network;
  peer_address_ = peer_address;
  socket_ = std::move(socket);
  writer_ = std::move(writer);
  net_log_ = net_log;
  probe_start_time_ = base::TimeTicks::Now();

  // |this| will listen to all socket write events for the probing
  // packet writer.
  writer_->set_delegate(this);
  reader_ = std::move(reader);
  initial_timeout_ = initial_timeout;

  net_log_.AddEvent(
      NetLogEventType::QUIC_CONNECTIVITY_PROBING_MANAGER_START_PROBING, [&] {
        return NetLogStartProbingParams(network_, &peer_address_,
                                        initial_timeout_);
      });

  reader_->StartReading();
  SendConnectivityProbingPacket(initial_timeout_);
}

void QuicConnectivityProbingManager::OnPacketReceived(
    const quic::QuicSocketAddress& self_address,
    const quic::QuicSocketAddress& peer_address,
    bool is_connectivity_probe) {
  DVLOG(1) << " *** " << __func__ << "() new packet received";
  DVLOG(1) << " is_connectivity_probe: " << is_connectivity_probe;
  DVLOG(1) << " peer_address: " << peer_address.ToString();
  DVLOG(1) << " self_address: " << self_address.ToString();
  if (!socket_) {
    DVLOG(1) << "Packet is ignored: probing is not live.";
    return;
  }

  IPEndPoint local_address;
  socket_->GetLocalAddress(&local_address);

  if (local_address != ToIPEndPoint(self_address) ||
      peer_address_ != peer_address) {
    DVLOG(1) << "Packet is ignored: probing is live at different path:";
    DVLOG(1) << " peer_address: " << local_address.ToString();
    DVLOG(1) << " self_address: " << self_address.ToString();
    return;
  }

  net_log_.AddEvent(
      NetLogEventType::QUIC_CONNECTIVITY_PROBING_MANAGER_PROBE_RECEIVED, [&] {
        return NetLogProbeReceivedParams(network_, &local_address,
                                         &peer_address_);
      });

  UMA_HISTOGRAM_COUNTS_100("Net.QuicSession.ProbingRetryCountUntilSuccess",
                           retry_count_);

  UMA_HISTOGRAM_TIMES("Net.QuicSession.ProbingTimeInMillisecondsUntilSuccess",
                      base::TimeTicks::Now() - probe_start_time_);

  // Notify the delegate that the probe succeeds and reset everything.
  delegate_->OnProbeSucceeded(network_, peer_address_, self_address,
                              std::move(socket_), std::move(writer_),
                              std::move(reader_));
  CancelProbingIfAny();
}

bool QuicConnectivityProbingManager::ValidateStatelessReset(
    const quic::QuicSocketAddress& self_address,
    const quic::QuicSocketAddress& peer_address) {
  IPEndPoint local_address;
  if (!socket_) {
    local_address = last_self_address_;
  } else {
    socket_->GetLocalAddress(&local_address);
  }

  if (local_address != ToIPEndPoint(self_address) ||
      peer_address_ != peer_address) {
    DVLOG(1) << "Probing lives at different path:";
    DVLOG(1) << " peer_address: " << local_address.ToString();
    DVLOG(1) << " self_address: " << self_address.ToString();
    return false;
  }

  if (is_running_) {
    stateless_reset_received_ = true;
  } else {
    UMA_HISTOGRAM_BOOLEAN("Net.QuicSession.StatelessResetAfterProbingCancelled",
                          true);
  }

  net_log_.AddEvent(
      NetLogEventType::
          QUIC_CONNECTIVITY_PROBING_MANAGER_STATELESS_RESET_RECEIVED,
      [&] {
        return NetLogProbeReceivedParams(network_, &local_address,
                                         &peer_address_);
      });

  NotifyDelegateProbeFailed();
  return true;
}

void QuicConnectivityProbingManager::SendConnectivityProbingPacket(
    base::TimeDelta timeout) {
  net_log_.AddEventWithInt64Params(
      NetLogEventType::QUIC_CONNECTIVITY_PROBING_MANAGER_PROBE_SENT,
      "sent_count", retry_count_);
  if (!delegate_->OnSendConnectivityProbingPacket(writer_.get(),
                                                  peer_address_)) {
    NotifyDelegateProbeFailed();
    return;
  }
  retransmit_timer_.Start(
      FROM_HERE, timeout,
      base::BindOnce(
          &QuicConnectivityProbingManager::MaybeResendConnectivityProbingPacket,
          weak_factory_.GetWeakPtr()));
}

void QuicConnectivityProbingManager::NotifyDelegateProbeFailed() {
  if (is_running_) {
    delegate_->OnProbeFailed(network_, peer_address_);
    CancelProbingIfAny();
  }
}

void QuicConnectivityProbingManager::MaybeResendConnectivityProbingPacket() {
  // Use exponential backoff for the timeout.
  retry_count_++;
  int64_t timeout_ms =
      (UINT64_C(1) << retry_count_) * initial_timeout_.InMilliseconds();
  if (timeout_ms > kMaxProbingTimeoutMs) {
    NotifyDelegateProbeFailed();
    return;
  }
  SendConnectivityProbingPacket(base::TimeDelta::FromMilliseconds(timeout_ms));
}

}  // namespace net
