/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "DHCPUtils.h"
#include <vector>
#include "mozilla/Logging.h"
#include "nsString.h"

#define MOZ_WORKING_BUFFER_SIZE_NETWORK_ADAPTERS 15000
#define MOZ_WORKING_BUFFER_SIZE_DHCP_PARAMS 1000
#define MOZ_MAX_TRIES 3
namespace mozilla {
namespace toolkit {
namespace system {
namespace windowsDHCPClient {

//
// The comments on this page reference the following Microsoft documentation
// pages (both retrieved 2017-06-27)
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa365915(v=vs.85).aspx
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa363298(v=vs.85).aspx
mozilla::LazyLogModule gDhcpUtilsLog("dhcpUtils");

#undef LOG
#define LOG(args) MOZ_LOG(gDhcpUtilsLog, LogLevel::Debug, args)

bool IsCurrentAndHasDHCP(PIP_ADAPTER_ADDRESSES aAddresses) {
  return aAddresses->OperStatus == IfOperStatusUp &&
         aAddresses->IfType != IF_TYPE_SOFTWARE_LOOPBACK &&
         (aAddresses->Flags & IP_ADAPTER_DHCP_ENABLED) &&
         (aAddresses->Dhcpv4Server.iSockaddrLength ||
          aAddresses->Dhcpv6Server.iSockaddrLength);
}

nsresult GetActiveDHCPNetworkAdapterName(
    nsACString& aNetworkAdapterName,
    WindowsNetworkFunctionsWrapper* aWindowsNetworkFunctionsWrapper) {
  /* Declare and initialize variables */

  uint32_t dwRetVal = 0;
  nsresult rv = NS_ERROR_FAILURE;

  // Set the flags to pass to GetAdaptersAddresses
  uint32_t flags = GAA_FLAG_INCLUDE_PREFIX;

  // default to unspecified address family (both)
  uint32_t family = AF_UNSPEC;

  // Allocate a 15 KB buffer to start with.
  uint32_t outBufLen = MOZ_WORKING_BUFFER_SIZE_NETWORK_ADAPTERS;
  uint32_t iterations = 0;

  aNetworkAdapterName.Truncate();

  // Now we try calling the GetAdaptersAddresses method until the return value
  // is not ERROR_BUFFER_OVERFLOW. According to [1]
  //
  //
  // > When the return value is ERROR_BUFFER_OVERFLOW, the SizePointer parameter
  // returned > points to the required size of the buffer to hold the adapter
  // information. > Note that it is possible for the buffer size required for
  // the IP_ADAPTER_ADDRESSES > structures pointed to by the AdapterAddresses
  // parameter to change between > subsequent calls to the GetAdaptersAddresses
  // function if an adapter address > is added or removed. However, this method
  // of using the GetAdaptersAddresses > function is strongly discouraged. This
  // method requires calling the > GetAdaptersAddresses function multiple times.
  // >
  // > The recommended method of calling the GetAdaptersAddresses function is
  // > to pre-allocate a 15KB working buffer pointed to by the AdapterAddresses
  // parameter. > On typical computers, this dramatically reduces the chances
  // that the > GetAdaptersAddresses function returns ERROR_BUFFER_OVERFLOW,
  // which would require > calling GetAdaptersAddresses function multiple times.
  //
  //
  // The possibility of the buffer size changing between calls to
  // GetAdaptersAddresses is why we allow the following code to be called
  // several times, rather than just the two that would be neccessary if we
  // could rely on the value returned in outBufLen being the true size needed.

  std::vector<IP_ADAPTER_ADDRESSES> pAddresses;
  do {
    // resize outBufLen up to the next multiple of sizeof(IP_ADAPTER_ADDRESSES)
    outBufLen = ((outBufLen + sizeof(IP_ADAPTER_ADDRESSES) - 1) /
                 sizeof(IP_ADAPTER_ADDRESSES)) *
                sizeof(IP_ADAPTER_ADDRESSES);
    pAddresses.resize(outBufLen / sizeof(IP_ADAPTER_ADDRESSES));
    LOG(
        ("Trying GetAdaptersAddresses with pAddresses sized to %zu and buffer "
         "length of %d",
         pAddresses.size(), outBufLen));

    dwRetVal = aWindowsNetworkFunctionsWrapper->GetAdaptersAddressesWrapped(
        family, flags, nullptr, pAddresses.data(), (PULONG)&outBufLen);

    if (dwRetVal == ERROR_BUFFER_OVERFLOW) {
      iterations++;
    }
  } while (dwRetVal == ERROR_BUFFER_OVERFLOW && iterations < MOZ_MAX_TRIES);

  switch (dwRetVal) {
    case NO_ERROR: {
      // set default return value if we don't find a suitable network adapter
      rv = NS_ERROR_NOT_AVAILABLE;
      PIP_ADAPTER_ADDRESSES pCurrAddresses = pAddresses.data();
      while (pCurrAddresses) {
        if (IsCurrentAndHasDHCP(pCurrAddresses)) {
          rv = NS_OK;
          aNetworkAdapterName.Assign(pCurrAddresses->AdapterName);
          break;
        }
        pCurrAddresses = pCurrAddresses->Next;
      }
    } break;
    case ERROR_NO_DATA:
      rv = NS_ERROR_NOT_AVAILABLE;
      break;
    default:
      MOZ_LOG(gDhcpUtilsLog, mozilla::LogLevel::Warning,
              ("GetAdaptersAddresses returned %d", dwRetVal));
      rv = NS_ERROR_FAILURE;
      break;
  }
  return rv;
}

DWORD
IterateDHCPInformRequestsUntilBufferLargeEnough(
    DHCPCAPI_PARAMS& aDhcpRequestedOptionParams,
    wchar_t* aWideNetworkAdapterName, std::vector<char>& aBuffer,
    WindowsNetworkFunctionsWrapper* aWindowsNetworkFunctionsWrapper) {
  uint32_t iterations = 0;
  uint32_t outBufLen = MOZ_WORKING_BUFFER_SIZE_DHCP_PARAMS;

  DHCPCAPI_PARAMS_ARRAY RequestParams = {1,  // only one option to request
                                         &aDhcpRequestedOptionParams};

  // According to [2],
  // the following is for 'Optional data to be requested,
  // in addition to the data requested in the RecdParams array.'
  // We are not requesting anything in addition, so this is empty.
  DHCPCAPI_PARAMS_ARRAY SendParams = {0, nullptr};

  DWORD winAPIResponse;
  // Now we try calling the DHCPRequestParams method until the return value
  // is not ERROR_MORE_DATA. According to [2]:
  //
  //
  // > Note that the required size of Buffer may increase during the time that
  // elapses > between the initial function call's return and a subsequent call;
  // > therefore, the required size of Buffer (indicated in pSize)
  // > provides an indication of the approximate size required of Buffer,
  // > rather than guaranteeing that subsequent calls will return successfully
  // > if Buffer is set to the size indicated in pSize.
  //
  //
  // This is why we allow this DHCPRequestParams to be called several times,
  // rather than just the two that would be neccessary if we could rely on the
  // value returned in outBufLen being the true size needed.
  do {
    aBuffer.resize(outBufLen);

    winAPIResponse = aWindowsNetworkFunctionsWrapper->DhcpRequestParamsWrapped(
        DHCPCAPI_REQUEST_SYNCHRONOUS,  // Flags
        nullptr,                       // Reserved
        aWideNetworkAdapterName,       // Adapter Name
        nullptr,                       // not using class id
        SendParams,                    // sent parameters
        RequestParams,                 // requesting params
        (PBYTE)aBuffer.data(),         // buffer for the output of RequestParams
        (PULONG)&outBufLen,            // buffer size
        nullptr  // Request ID for persistent requests - not needed here
    );

    if (winAPIResponse == ERROR_MORE_DATA) {
      iterations++;
    }
  } while (winAPIResponse == ERROR_MORE_DATA && iterations < MOZ_MAX_TRIES);
  return winAPIResponse;
}

nsresult RetrieveOption(
    const nsACString& aAdapterName, uint8_t aOption,
    std::vector<char>& aOptionValueBuf, uint32_t* aOptionSize,
    WindowsNetworkFunctionsWrapper* aWindowsNetworkFunctionsWrapper) {
  nsresult rv;
  nsAutoString wideNetworkAdapterName = NS_ConvertUTF8toUTF16(aAdapterName);

  DHCPCAPI_PARAMS DhcpRequestedOptionParams = {
      0,        //  Flags - Reserved, must be set to zero [2]
      aOption,  // OptionId
      false,    // whether this is vendor specific - let's assume not
      nullptr,  // data filled in on return
      0         // nBytes used by return data
  };

  std::vector<char> tmpBuffer(
      MOZ_WORKING_BUFFER_SIZE_DHCP_PARAMS);  // a buffer for the DHCP response
                                             // object
  DWORD winAPIResponse = IterateDHCPInformRequestsUntilBufferLargeEnough(
      DhcpRequestedOptionParams, wideNetworkAdapterName.get(), tmpBuffer,
      aWindowsNetworkFunctionsWrapper);

  switch (winAPIResponse) {
    case NO_ERROR: {
      if (DhcpRequestedOptionParams.nBytesData == 0) {
        *aOptionSize = 0;
        rv = NS_ERROR_NOT_AVAILABLE;
        break;
      }

      if (*aOptionSize >= DhcpRequestedOptionParams.nBytesData) {
        rv = NS_OK;
      } else {
        rv = NS_ERROR_LOSS_OF_SIGNIFICANT_DATA;
      }

      uint32_t actualSizeReturned =
          *aOptionSize > DhcpRequestedOptionParams.nBytesData
              ? DhcpRequestedOptionParams.nBytesData
              : *aOptionSize;

      memcpy(aOptionValueBuf.data(), DhcpRequestedOptionParams.Data,
             actualSizeReturned);
      *aOptionSize = DhcpRequestedOptionParams.nBytesData;
      break;
    }
    case ERROR_INVALID_PARAMETER:
      MOZ_LOG(gDhcpUtilsLog, mozilla::LogLevel::Warning,
              ("RetrieveOption returned %lu (ERROR_INVALID_PARAMETER) when "
               "option %d requested",
               winAPIResponse, aOption));
      rv = NS_ERROR_INVALID_ARG;
      break;
    default:
      MOZ_LOG(gDhcpUtilsLog, mozilla::LogLevel::Warning,
              ("RetrieveOption returned %lu when option %d requested",
               winAPIResponse, aOption));
      rv = NS_ERROR_FAILURE;
  }
  return rv;
}

}  // namespace windowsDHCPClient
}  // namespace system
}  // namespace toolkit
}  // namespace mozilla
