// Copyright (c) 2010 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.

// Provides MAC addresses of connected routers by using the /proc/net/
// directory which contains files with network information.
// This directory is used in most Linux based operating systems.

#include "chrome/browser/geolocation/gateway_data_provider_linux.h"

#include <algorithm>
#include <arpa/inet.h>
#include <net/if.h>
#include <net/if_arp.h>
#include <set>
#include <string>
#include <sys/ioctl.h>
#include <vector>

#include "base/command_line.h"
#include "base/file_util.h"
#include "base/string_number_conversions.h"
#include "base/string_tokenizer.h"
#include "base/string_util.h"
#include "base/utf_string_conversions.h"
#include "chrome/browser/geolocation/empty_device_data_provider.h"
#include "chrome/browser/geolocation/gateway_data_provider_common.h"
#include "chrome/common/chrome_switches.h"

namespace {
const unsigned int kMaxArpIterations = 30;
const unsigned int kMaxRouteIterations = 20;
const char kNoGateway[] = "00000000";
const char kArpFilePath[] = "/proc/net/arp";
const char kRouteFilePath[] = "/proc/net/route";

// Converts an IP address held in a little endian paired hex format to period
// separated decimal format. For example 0101A8C0 becomes 192.168.1.1
std::string HexToIpv4Format(std::string& hex_address) {
  if (hex_address.size() != 8)
    return "";
  std::vector<uint8> bytes;
  base::HexStringToBytes(hex_address, &bytes);
  if (bytes.size() != 4)
    return "";
  struct in_addr in;
  uint32 ip_native_endian;
  memcpy(&ip_native_endian, &bytes[0], 4);
  in.s_addr = htonl(ip_native_endian);
  return inet_ntoa(in);
}

// TODO(joth): Cache the sets of gateways and MAC addresses to avoid reading
// through the arp table when the routing table hasn't changed.
class LinuxGatewayApi : public GatewayDataProviderCommon::GatewayApiInterface {
 public:
  LinuxGatewayApi() {}
  virtual ~LinuxGatewayApi() {}

  static LinuxGatewayApi* Create() {
    return new LinuxGatewayApi();
  }

  // GatewayApiInterface
  virtual bool GetRouterData(GatewayData::RouterDataSet* data);

 private:
  DISALLOW_COPY_AND_ASSIGN(LinuxGatewayApi);
};

// Gets a set of gateways using data from /proc/net/route
// Returns false if we cannot read the route file or we cannot classify the
// type of network adapter.
// Routing data is held in the following format
// Note: "|" represents a delimeter.
// Iface|Destination|Gateway|Flags|RefCnt|Use|Metric|Mask|MTU|Window|IRTT
// The delimiter for this table is "\t".
// ioctl calls are used to verify the type of network adapter, using interface
// names from the route table.
bool GetGateways(std::set<std::string>* gateways) {
  FilePath route_file_path(kRouteFilePath);
  std::string route_file;
  if (!file_util::ReadFileToString(route_file_path, &route_file)) {
    // Unable to read file.
    return false;
  }
  StringTokenizer tokenized_route_file(route_file, "\n");
  std::vector<std::string> route_data;
  // Remove column labels.
  tokenized_route_file.GetNext();

  // Get rows of data.
  while (tokenized_route_file.GetNext() &&
         route_data.size() < kMaxRouteIterations)
    route_data.push_back(tokenized_route_file.token());

  if (route_data.empty()) {
    // Having no data is not an error case.
    return true;
  }

  int sock;
  sock = socket(AF_INET, SOCK_STREAM, 0);
  if (sock == -1)
    return false;
  struct ifreq network_device;

  for (size_t j = 0; j < route_data.size(); ++j) {
    // Get gateway address.
    std::string gateway;
    StringTokenizer tokenized_route_data(route_data[j], "\t");
    // Check if device is an Ethernet device.
    if (!tokenized_route_data.GetNext()) // Get Iface.
      continue;
    // Ensure space for the null term too.
    if (tokenized_route_data.token().size() >=
        arraysize(network_device.ifr_name))
      continue;
    memset(&network_device, 0, sizeof(network_device));
    base::strlcpy(network_device.ifr_name,
                  tokenized_route_data.token().c_str(),
                  arraysize(network_device.ifr_name));
    if (ioctl(sock, SIOCGIFHWADDR,
        reinterpret_cast<char*>(&network_device)) < 0) {
      // Could not get device type.
      continue;
    }
    if (network_device.ifr_hwaddr.sa_family != ARPHRD_ETHER) {
      // Device is not an Ethernet device.
      continue;
    }
    if (!tokenized_route_data.GetNext()) // Skip Destination.
          continue;
    if (!tokenized_route_data.GetNext()) // Get Gateway.
          continue;
    gateway = tokenized_route_data.token();
    if (gateway != kNoGateway) {
      // TODO(joth): Currently only works for gateways using IPv4.
      std::string gateway_int_format = HexToIpv4Format(gateway);
      if (!gateway_int_format.empty())
        gateways->insert(gateway_int_format);
    }
  }

  close(sock);
  return true;
}

// Gets a RouterDataSet containing MAC addresses related to any connected
// routers. Returns false if we cannot read the arp file.
// The /proc/net/arp file contains arp data in the following format
// Note: "|" represents a delimeter.
// IP address|HW type|Flags|HW address|Mask|Device
// The delimiter for this table is " ".
bool GetMacAddresses(GatewayData::RouterDataSet* data,
                     std::set<std::string>* gateways) {
  // Find MAC addresses of routers
  FilePath arp_file_path(kArpFilePath);
  std::string arp_file_out;
  if (!file_util::ReadFileToString(arp_file_path, &arp_file_out)) {
    // Unable to read file.
    return false;
  }
  StringTokenizer tokenized_arp_file(arp_file_out, "\n");
  std::vector<std::string> arp_data;
  // Remove column labels.
  tokenized_arp_file.GetNext();

  // Get rows of data.
  while (tokenized_arp_file.GetNext() && arp_data.size() < kMaxArpIterations)
    arp_data.push_back(tokenized_arp_file.token());

  for (size_t k = 0; k < arp_data.size(); k++) {
    std::string ip_address;
    StringTokenizer tokenized_arp_data(arp_data[k], " ");
    if (!tokenized_arp_data.GetNext()) // Get IP address.
      continue;
    ip_address = tokenized_arp_data.token();
    if (gateways->find(ip_address) == gateways->end())
      continue;
    if (!tokenized_arp_data.GetNext()) // Skip HW type.
      continue;
    if (!tokenized_arp_data.GetNext()) // Skip flags.
      continue;
    if (!tokenized_arp_data.GetNext()) // Get HW address.
      continue;
    RouterData router;
    std::string mac_add;
    mac_add = tokenized_arp_data.token();
    std::replace(mac_add.begin(), mac_add.end(), ':', '-');
    router.mac_address = UTF8ToUTF16(mac_add);
    data->insert(router);
  }

  return true;
}

bool LinuxGatewayApi::GetRouterData(GatewayData::RouterDataSet* data) {
  std::set<std::string> gateways;
  if (!GetGateways(&gateways))
    return false;
  if (gateways.empty())
    return true;
  return GetMacAddresses(data, &gateways);
}
} // namespace

// static
template<>
GatewayDataProviderImplBase* GatewayDataProvider::DefaultFactoryFunction() {
  if (!CommandLine::ForCurrentProcess()
      ->HasSwitch(switches::kExperimentalLocationFeatures))
    return new EmptyDeviceDataProvider<GatewayData>();
  return new GatewayDataProviderLinux();
}

GatewayDataProviderLinux::GatewayDataProviderLinux() {
}

GatewayDataProviderLinux::~GatewayDataProviderLinux() {
}

GatewayDataProviderCommon::GatewayApiInterface*
    GatewayDataProviderLinux::NewGatewayApi() {
  return LinuxGatewayApi::Create();
}