/*
   +----------------------------------------------------------------------+
   | HipHop for PHP                                                       |
   +----------------------------------------------------------------------+
   | Copyright (c) 2010-2015 Facebook, Inc. (http://www.facebook.com)     |
   +----------------------------------------------------------------------+
   | This source file is subject to version 3.01 of the PHP license,      |
   | that is bundled with this package in the file LICENSE, and is        |
   | available through the world-wide-web at the following url:           |
   | http://www.php.net/license/3_01.txt                                  |
   | If you did not receive a copy of the PHP license and are unable to   |
   | obtain it through the world-wide-web, please send a note to          |
   | license@php.net so we can mail you a copy immediately.               |
   +----------------------------------------------------------------------+
*/

#include "hphp/runtime/server/ip-block-map.h"

#include <arpa/inet.h>

#include "hphp/util/logger.h"
#include "hphp/runtime/base/config.h"

namespace HPHP {
///////////////////////////////////////////////////////////////////////////////

IpBlockMap::Acl::Acl() : m_networks(true) {}

IpBlockMap::BinaryPrefixTrie::BinaryPrefixTrie(bool allow) {
  m_children[0] = m_children[1] = nullptr;
  setAllowed(allow);
}

void IpBlockMap::BinaryPrefixTrie::setAllowed(bool allow) {
  m_allow = allow;
}

bool IpBlockMap::BinaryPrefixTrie::isAllowed(
    const void *search,
    const int num_bits) {
  return isAllowedImpl(search, num_bits, 0);
}

bool IpBlockMap::BinaryPrefixTrie::isAllowedImpl(
    const void *search,
    const int num_bits,
    const int bit_offset) {
  const unsigned char *search_bytes = (const unsigned char *)search;
  BinaryPrefixTrie *child;

  if (bit_offset == num_bits) {
    if (m_children[0] != nullptr || m_children[1] != nullptr) {
      // This should never happen because the trie should only ever contain
      // prefixes of fixed-size network addresses, so the trie should never be
      // any deeper than the network address size.
      Logger::Error("trie depth exceeds search depth");
      return false;
    }
    return m_allow;
  }

  assert(bit_offset < num_bits);

  child = m_children[(*search_bytes >> (7 - bit_offset)) & 1];
  if (child) {
    if (bit_offset < 7) {
      return child->isAllowedImpl(search_bytes, num_bits, bit_offset + 1);
    } else {
      return child->isAllowedImpl(search_bytes + 1, num_bits - 8, 0);
    }
  }

  return m_allow;
}

void IpBlockMap::BinaryPrefixTrie::InsertNewPrefix(
    BinaryPrefixTrie *root,
    const void *value,
    const int num_bits,
    bool allow) {
  const unsigned char *bytes = (const unsigned char *)value;
  BinaryPrefixTrie *node = root;
  int curr_bit_num = 0;
  int curr_bit_val;
  bool next_allow = root->m_allow;

  while (curr_bit_num < num_bits) {
    // Peel off the bit at the current position
    curr_bit_val = (bytes[curr_bit_num / 8] >> (7 - (curr_bit_num & 7))) & 1;
    curr_bit_num++;
    if (!node->m_children[curr_bit_val]) {
      // When inserting the leaf node, stop inheriting the "allow" value
      // from ancestor nodes.
      if (curr_bit_num == num_bits) {
        next_allow = allow;
      }
      BinaryPrefixTrie *new_node = new BinaryPrefixTrie(next_allow);
      node->m_children[curr_bit_val] = new_node;
    }

    node = node->m_children[curr_bit_val];
    next_allow = node->m_allow;
  }
}

bool IpBlockMap::ReadIPv6Address(const char *text,
                                 struct in6_addr *output,
                                 int &significant_bits) {
#define STRING_IPV4_ADDR_MAX_LENGTH 15
#define STRING_IPV6_ADDR_MAX_LENGTH 39
  char address[STRING_IPV6_ADDR_MAX_LENGTH + 1];
  int address_len;
  const char *slash;
  bool is_ipv6 = (nullptr != strchr(text, ':'));

  // Find the bit count, if any.
  slash = strchr(text, '/');

  if (slash) {
    significant_bits = atoi(slash + 1);
    if (!significant_bits) {
      Logger::Error("invalid bit count: %s", text);
      return false;
    }
    address_len = slash - text;
  } else {
    significant_bits = is_ipv6 ? 128 : 32;
    address_len = strlen(text);
  }

  if (is_ipv6) {
    memcpy(address, text, address_len);
    address[address_len] = '\0';
  } else {
    // An IPv4 mapped address.
    if (address_len > STRING_IPV4_ADDR_MAX_LENGTH) {
      Logger::Error("invalid IPv4 address: %s", text);
      return false;
    }
    memcpy(address, "::ffff:", 7);
    memcpy(address + 7, text, address_len);
    address[address_len + 7] = '\0';
    significant_bits += 96;
  }

  if (inet_pton(AF_INET6, address, output) <= 0) {
    Logger::Error("invalid IPv6 address: %s", address);
    return false;
  }

  return true;
}

void IpBlockMap::LoadIpList(std::shared_ptr<Acl> acl,
                            const IniSetting::Map& ini, const Hdf& hdf,
                            const std::string& name, bool allow) {
  auto ipm_lil_callback = [acl, allow] (const IniSetting::Map &ini_ipm_lil,
                                        const Hdf &hdf_ipm_lil,
                                        const std::string& ini_ipm_name) {
    std::string ip = Config::GetString(ini_ipm_lil, hdf_ipm_lil, ini_ipm_name,
                                       "", false);

    int bits;
    struct in6_addr address;
    if (ReadIPv6Address(ip.c_str(), &address, bits)) {
      BinaryPrefixTrie::InsertNewPrefix(&acl->m_networks,
                                        &address,
                                        bits,
                                        allow);
    }
  };
  Config::Iterate(ipm_lil_callback, ini, hdf, name, false);
}

IpBlockMap::IpBlockMap(const IniSetting::Map& ini, const Hdf& config) {
  auto ipm_callback = [&] (const IniSetting::Map &ini_ipm,
                           const Hdf &hdf_ipm,
                           const std::string& ini_name) {
    auto acl = std::make_shared<Acl>();
    // sgrimm note: not sure AllowFirst is relevant with my implementation
    // since we always search for the narrowest matching rule -- it really
    // just sets whether we deny or allow by default, I think.
    bool allow = Config::GetBool(ini_ipm, hdf_ipm, "AllowFirst", false, false);
    if (allow) {
      acl->m_networks.setAllowed(true);
      LoadIpList(acl, ini_ipm, hdf_ipm, "Ip.Deny", false);
      LoadIpList(acl, ini_ipm, hdf_ipm, "Ip.Allow", true);
    } else {
      acl->m_networks.setAllowed(false);
      LoadIpList(acl, ini_ipm, hdf_ipm, "Ip.Allow", true);
      LoadIpList(acl, ini_ipm, hdf_ipm, "Ip.Deny", false);
    }

    std::string location = Config::GetString(ini_ipm, hdf_ipm, "Location", "",
                                             false);
    if (!location.empty() && location[0] == '/') {
      location = location.substr(1);
    }
    m_acls[location] = acl;
  };
  Config::Iterate(ipm_callback, ini, config, "IpBlockMap", true);
}

bool IpBlockMap::isBlocking(const std::string &command,
                            const std::string &ip) const {
  bool translated = false;
  struct in6_addr address;
  int bits;

  for (auto iter = m_acls.begin(); iter != m_acls.end(); ++iter) {
    const std::string &path = iter->first;
    if (command.size() >= path.size() &&
        strncmp(command.c_str(), path.c_str(), path.size()) == 0) {

      if (!translated) {
        ReadIPv6Address(ip.c_str(), &address, bits);
        assert(bits == 128);
        translated = true;
      }

      return !iter->second->m_networks.isAllowed(&address);
    }
  }
  return false;
}

///////////////////////////////////////////////////////////////////////////////
}
