// Copyright 2013 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 "chrome/browser/printing/cloud_print/privet_traffic_detector.h"

#include <stddef.h>

#include "base/location.h"
#include "base/macros.h"
#include "base/metrics/histogram_macros.h"
#include "base/single_thread_task_runner.h"
#include "base/sys_byteorder.h"
#include "base/task_scheduler/post_task.h"
#include "base/threading/thread_restrictions.h"
#include "base/threading/thread_task_runner_handle.h"
#include "chrome/browser/browser_process.h"
#include "net/base/ip_address.h"
#include "net/base/net_errors.h"
#include "net/base/network_change_notifier.h"
#include "net/base/network_interfaces.h"
#include "net/dns/dns_protocol.h"
#include "net/dns/dns_response.h"
#include "net/dns/mdns_client.h"
#include "net/log/net_log_source.h"
#include "net/socket/datagram_server_socket.h"
#include "net/socket/udp_server_socket.h"

namespace {

const int kMaxRestartAttempts = 10;
const char kPrivetDeviceTypeDnsString[] = "\x07_privet";

void GetNetworkListInBackground(
    const base::Callback<void(const net::NetworkInterfaceList&)> callback) {
  base::AssertBlockingAllowed();
  net::NetworkInterfaceList networks;
  if (!GetNetworkList(&networks, net::INCLUDE_HOST_SCOPE_VIRTUAL_INTERFACES))
    return;

  net::NetworkInterfaceList ip4_networks;
  for (size_t i = 0; i < networks.size(); ++i) {
    net::AddressFamily address_family =
        net::GetAddressFamily(networks[i].address);
    if (address_family == net::ADDRESS_FAMILY_IPV4 &&
        networks[i].prefix_length >= 24) {
      ip4_networks.push_back(networks[i]);
    }
  }

  net::IPAddress localhost_prefix(127, 0, 0, 0);
  ip4_networks.push_back(
      net::NetworkInterface("lo",
                            "lo",
                            0,
                            net::NetworkChangeNotifier::CONNECTION_UNKNOWN,
                            localhost_prefix,
                            8,
                            net::IP_ADDRESS_ATTRIBUTE_NONE));
  content::BrowserThread::PostTask(content::BrowserThread::IO, FROM_HERE,
                                   base::BindOnce(callback, ip4_networks));
}

}  // namespace

namespace cloud_print {

PrivetTrafficDetector::PrivetTrafficDetector(
    net::AddressFamily address_family,
    const base::Closure& on_traffic_detected)
    : on_traffic_detected_(on_traffic_detected),
      callback_runner_(base::ThreadTaskRunnerHandle::Get()),
      address_family_(address_family),
      io_buffer_(
          new net::IOBufferWithSize(net::dns_protocol::kMaxMulticastSize)),
      restart_attempts_(kMaxRestartAttempts),
      weak_ptr_factory_(this) {}

PrivetTrafficDetector::~PrivetTrafficDetector() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
}

void PrivetTrafficDetector::Start() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  g_browser_process->network_connection_tracker()->AddNetworkConnectionObserver(
      this);
  content::BrowserThread::PostTask(
      content::BrowserThread::IO, FROM_HERE,
      base::BindOnce(&PrivetTrafficDetector::ScheduleRestart,
                     weak_ptr_factory_.GetWeakPtr()));
}

void PrivetTrafficDetector::Stop() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  g_browser_process->network_connection_tracker()
      ->RemoveNetworkConnectionObserver(this);
}

void PrivetTrafficDetector::OnConnectionChanged(
    network::mojom::ConnectionType type) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  content::BrowserThread::PostTask(
      content::BrowserThread::IO, FROM_HERE,
      base::BindOnce(&PrivetTrafficDetector::HandleConnectionChanged,
                     weak_ptr_factory_.GetWeakPtr(), type));
}

void PrivetTrafficDetector::HandleConnectionChanged(
    network::mojom::ConnectionType type) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
  restart_attempts_ = kMaxRestartAttempts;
  if (type != network::mojom::ConnectionType::CONNECTION_NONE) {
    ScheduleRestart();
  }
}

void PrivetTrafficDetector::ScheduleRestart() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
  socket_.reset();
  weak_ptr_factory_.InvalidateWeakPtrs();
  base::PostDelayedTaskWithTraits(
      FROM_HERE, {base::TaskPriority::BACKGROUND, base::MayBlock()},
      base::BindOnce(&GetNetworkListInBackground,
                     base::Bind(&PrivetTrafficDetector::Restart,
                                weak_ptr_factory_.GetWeakPtr())),
      base::TimeDelta::FromSeconds(3));
}

void PrivetTrafficDetector::Restart(const net::NetworkInterfaceList& networks) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
  networks_ = networks;
  if (Bind() < net::OK || DoLoop(0) < net::OK) {
    if ((restart_attempts_--) > 0)
      ScheduleRestart();
  } else {
    // Reset on success.
    restart_attempts_ = kMaxRestartAttempts;
  }
}

int PrivetTrafficDetector::Bind() {
  if (!start_time_.is_null()) {
    base::TimeDelta time_delta = base::Time::Now() - start_time_;
    UMA_HISTOGRAM_LONG_TIMES("LocalDiscovery.DetectorRestartTime", time_delta);
  }
  start_time_ = base::Time::Now();
  DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
  socket_.reset(new net::UDPServerSocket(NULL, net::NetLogSource()));
  net::IPEndPoint multicast_addr = net::GetMDnsIPEndPoint(address_family_);
  net::IPEndPoint bind_endpoint(
      net::IPAddress::AllZeros(multicast_addr.address().size()),
      multicast_addr.port());
  socket_->AllowAddressReuse();
  int rv = socket_->Listen(bind_endpoint);
  if (rv < net::OK)
    return rv;
  socket_->SetMulticastLoopbackMode(false);
  return socket_->JoinGroup(multicast_addr.address());
}

bool PrivetTrafficDetector::IsSourceAcceptable() const {
  for (size_t i = 0; i < networks_.size(); ++i) {
    if (net::IPAddressMatchesPrefix(recv_addr_.address(), networks_[i].address,
                                    networks_[i].prefix_length)) {
      return true;
    }
  }
  return false;
}

bool PrivetTrafficDetector::IsPrivetPacket(int rv) const {
  if (rv <= static_cast<int>(sizeof(net::dns_protocol::Header)) ||
      !IsSourceAcceptable()) {
    return false;
  }

  const char* buffer_begin = io_buffer_->data();
  const char* buffer_end = buffer_begin + rv;
  const net::dns_protocol::Header* header =
      reinterpret_cast<const net::dns_protocol::Header*>(buffer_begin);
  // Check if response packet.
  if (!(header->flags & base::HostToNet16(net::dns_protocol::kFlagResponse)))
    return false;
  const char* substring_begin = kPrivetDeviceTypeDnsString;
  const char* substring_end = substring_begin +
                              arraysize(kPrivetDeviceTypeDnsString) - 1;
  // Check for expected substring, any Privet device must include this.
  return std::search(buffer_begin, buffer_end, substring_begin,
                     substring_end) != buffer_end;
}

int PrivetTrafficDetector::DoLoop(int rv) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
  do {
    if (IsPrivetPacket(rv)) {
      socket_.reset();
      callback_runner_->PostTask(FROM_HERE, on_traffic_detected_);
      base::TimeDelta time_delta = base::Time::Now() - start_time_;
      UMA_HISTOGRAM_LONG_TIMES("LocalDiscovery.DetectorTriggerTime",
                               time_delta);
      return net::OK;
    }

    rv = socket_->RecvFrom(
        io_buffer_.get(),
        io_buffer_->size(),
        &recv_addr_,
        base::Bind(base::IgnoreResult(&PrivetTrafficDetector::DoLoop),
                   base::Unretained(this)));
  } while (rv > 0);

  if (rv != net::ERR_IO_PENDING)
    return rv;

  return net::OK;
}

}  // namespace cloud_print
