/** @file
  iSCSI DHCP4 related configuration routines.

Copyright (c) 2004 - 2011, Intel Corporation. All rights reserved.<BR>
This program and the accompanying materials
are licensed and made available under the terms and conditions of the BSD License
which accompanies this distribution.  The full text of the license may be found at
http://opensource.org/licenses/bsd-license.php

THE PROGRAM IS DISTRIBUTED UNDER THE BSD LICENSE ON AN "AS IS" BASIS,
WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED.

**/

#include "IScsiImpl.h"


/**
  Extract the Root Path option and get the required target information.

  @param[in]        RootPath         The RootPath.
  @param[in]        Length           Length of the RootPath option payload.
  @param[in, out]   ConfigData       The iSCSI attempt configuration data read
                                     from a nonvolatile device.

  @retval EFI_SUCCESS           All required information is extracted from the RootPath option.
  @retval EFI_NOT_FOUND         The RootPath is not an iSCSI RootPath.
  @retval EFI_OUT_OF_RESOURCES  Failed to allocate memory.
  @retval EFI_INVALID_PARAMETER The RootPath is malformatted.

**/
EFI_STATUS
IScsiDhcpExtractRootPath (
  IN      CHAR8                        *RootPath,
  IN      UINT8                        Length,
  IN OUT  ISCSI_ATTEMPT_CONFIG_NVDATA *ConfigData
  )
{
  EFI_STATUS                  Status;
  UINT8                       IScsiRootPathIdLen;
  CHAR8                       *TmpStr;
  ISCSI_ROOT_PATH_FIELD       Fields[RP_FIELD_IDX_MAX];
  ISCSI_ROOT_PATH_FIELD       *Field;
  UINT32                      FieldIndex;
  UINT8                       Index;
  ISCSI_SESSION_CONFIG_NVDATA *ConfigNvData;
  EFI_IP_ADDRESS              Ip;
  UINT8                       IpMode;

  ConfigNvData = &ConfigData->SessionConfigData;

  //
  // "iscsi:"<servername>":"<protocol>":"<port>":"<LUN>":"<targetname>
  //
  IScsiRootPathIdLen = (UINT8) AsciiStrLen (ISCSI_ROOT_PATH_ID);

  if ((Length <= IScsiRootPathIdLen) || (CompareMem (RootPath, ISCSI_ROOT_PATH_ID, IScsiRootPathIdLen) != 0)) {
    return EFI_NOT_FOUND;
  }
  //
  // Skip the iSCSI RootPath ID "iscsi:".
  //
  RootPath += IScsiRootPathIdLen;
  Length  = (UINT8) (Length - IScsiRootPathIdLen);

  TmpStr  = (CHAR8 *) AllocatePool (Length + 1);
  if (TmpStr == NULL) {
    return EFI_OUT_OF_RESOURCES;
  }

  CopyMem (TmpStr, RootPath, Length);
  TmpStr[Length]  = '\0';

  Index           = 0;
  FieldIndex      = RP_FIELD_IDX_SERVERNAME;
  ZeroMem (&Fields[0], sizeof (Fields));

  //
  // Extract the fields in the Root Path option string.
  //
  for (FieldIndex = RP_FIELD_IDX_SERVERNAME; (FieldIndex < RP_FIELD_IDX_MAX) && (Index < Length); FieldIndex++) {
    if (TmpStr[Index] != ISCSI_ROOT_PATH_FIELD_DELIMITER) {
      Fields[FieldIndex].Str = &TmpStr[Index];
    }

    while ((TmpStr[Index] != ISCSI_ROOT_PATH_FIELD_DELIMITER) && (Index < Length)) {
      Index++;
    }

    if (TmpStr[Index] == ISCSI_ROOT_PATH_FIELD_DELIMITER) {
      if (FieldIndex != RP_FIELD_IDX_TARGETNAME) {
        TmpStr[Index] = '\0';
        Index++;
      }

      if (Fields[FieldIndex].Str != NULL) {
        Fields[FieldIndex].Len = (UINT8) AsciiStrLen (Fields[FieldIndex].Str);
      }
    }
  }

  if (FieldIndex != RP_FIELD_IDX_MAX) {
    Status = EFI_INVALID_PARAMETER;
    goto ON_EXIT;
  }

  if ((Fields[RP_FIELD_IDX_SERVERNAME].Str == NULL) ||
      (Fields[RP_FIELD_IDX_TARGETNAME].Str == NULL) ||
      (Fields[RP_FIELD_IDX_PROTOCOL].Len > 1)
      ) {

    Status = EFI_INVALID_PARAMETER;
    goto ON_EXIT;
  }
  //
  // Get the IP address of the target.
  //
  Field   = &Fields[RP_FIELD_IDX_SERVERNAME];

  if (ConfigNvData->IpMode < IP_MODE_AUTOCONFIG) {
    IpMode = ConfigNvData->IpMode;
  } else {
    IpMode = ConfigData->AutoConfigureMode;
  }

  Status = IScsiAsciiStrToIp (Field->Str, IpMode, &Ip);
  CopyMem (&ConfigNvData->TargetIp, &Ip, sizeof (EFI_IP_ADDRESS));

  if (EFI_ERROR (Status)) {
    goto ON_EXIT;
  }
  //
  // Check the protocol type.
  //
  Field = &Fields[RP_FIELD_IDX_PROTOCOL];
  if ((Field->Str != NULL) && ((*(Field->Str) - '0') != EFI_IP_PROTO_TCP)) {
    Status = EFI_INVALID_PARAMETER;
    goto ON_EXIT;
  }
  //
  // Get the port of the iSCSI target.
  //
  Field = &Fields[RP_FIELD_IDX_PORT];
  if (Field->Str != NULL) {
    ConfigNvData->TargetPort = (UINT16) AsciiStrDecimalToUintn (Field->Str);
  } else {
    ConfigNvData->TargetPort = ISCSI_WELL_KNOWN_PORT;
  }
  //
  // Get the LUN.
  //
  Field = &Fields[RP_FIELD_IDX_LUN];
  if (Field->Str != NULL) {
    Status = IScsiAsciiStrToLun (Field->Str, ConfigNvData->BootLun);
    if (EFI_ERROR (Status)) {
      goto ON_EXIT;
    }
  } else {
    ZeroMem (ConfigNvData->BootLun, sizeof (ConfigNvData->BootLun));
  }
  //
  // Get the target iSCSI Name.
  //
  Field = &Fields[RP_FIELD_IDX_TARGETNAME];

  if (AsciiStrLen (Field->Str) > ISCSI_NAME_MAX_SIZE - 1) {
    Status = EFI_INVALID_PARAMETER;
    goto ON_EXIT;
  }
  //
  // Validate the iSCSI name.
  //
  Status = IScsiNormalizeName (Field->Str, AsciiStrLen (Field->Str));
  if (EFI_ERROR (Status)) {
    goto ON_EXIT;
  }

  AsciiStrCpy (ConfigNvData->TargetName, Field->Str);

ON_EXIT:

  FreePool (TmpStr);

  return Status;
}

/**
  The callback function registerd to the DHCP4 instance that is used to select
  the qualified DHCP OFFER.

  @param[in]  This         The DHCP4 protocol.
  @param[in]  Context      The context set when configuring the DHCP4 protocol.
  @param[in]  CurrentState The current state of the DHCP4 protocol.
  @param[in]  Dhcp4Event   The event occurs in the current state.
  @param[in]  Packet       The DHCP packet that is to be sent or was already received.
  @param[out] NewPacket    The packet used to replace the above Packet.

  @retval EFI_SUCCESS      Either the DHCP OFFER is qualified or we're not intereseted
                           in the Dhcp4Event.
  @retval EFI_NOT_READY    The DHCP OFFER packet doesn't match our requirements.
  @retval Others           Other errors as indicated.

**/
EFI_STATUS
EFIAPI
IScsiDhcpSelectOffer (
  IN  EFI_DHCP4_PROTOCOL  *This,
  IN  VOID                *Context,
  IN  EFI_DHCP4_STATE     CurrentState,
  IN  EFI_DHCP4_EVENT     Dhcp4Event,
  IN  EFI_DHCP4_PACKET    *Packet, OPTIONAL
  OUT EFI_DHCP4_PACKET    **NewPacket OPTIONAL
  )
{
  EFI_STATUS              Status;
  UINT32                  OptionCount;
  EFI_DHCP4_PACKET_OPTION **OptionList;
  UINT32                  Index;

  if ((Dhcp4Event != Dhcp4RcvdOffer) && (Dhcp4Event != Dhcp4SelectOffer)) {
    return EFI_SUCCESS;
  }

  OptionCount = 0;

  Status      = This->Parse (This, Packet, &OptionCount, NULL);
  if (Status != EFI_BUFFER_TOO_SMALL) {
    return EFI_NOT_READY;
  }

  OptionList = AllocatePool (OptionCount * sizeof (EFI_DHCP4_PACKET_OPTION *));
  if (OptionList == NULL) {
    return EFI_NOT_READY;
  }

  Status = This->Parse (This, Packet, &OptionCount, OptionList);
  if (EFI_ERROR (Status)) {
    FreePool (OptionList);
    return EFI_NOT_READY;
  }

  for (Index = 0; Index < OptionCount; Index++) {
    if (OptionList[Index]->OpCode != DHCP4_TAG_ROOT_PATH) {
      continue;
    }

    Status = IScsiDhcpExtractRootPath (
               (CHAR8 *) &OptionList[Index]->Data[0],
               OptionList[Index]->Length,
               (ISCSI_ATTEMPT_CONFIG_NVDATA *) Context
               );

    break;
  }

  if ((Index == OptionCount)) {
    Status = EFI_NOT_READY;
  }

  FreePool (OptionList);

  return Status;
}

/**
  Parse the DHCP ACK to get the address configuration and DNS information.

  @param[in]       Dhcp4        The DHCP4 protocol.
  @param[in, out]  ConfigData   The session configuration data.

  @retval EFI_SUCCESS           The DNS information is got from the DHCP ACK.
  @retval EFI_NO_MAPPING        DHCP failed to acquire address and other information.
  @retval EFI_INVALID_PARAMETER The DHCP ACK's DNS option is malformatted.
  @retval EFI_DEVICE_ERROR      Other errors as indicated.
  @retval EFI_OUT_OF_RESOURCES  Failed to allocate memory.

**/
EFI_STATUS
IScsiParseDhcpAck (
  IN     EFI_DHCP4_PROTOCOL          *Dhcp4,
  IN OUT ISCSI_ATTEMPT_CONFIG_NVDATA *ConfigData
  )
{
  EFI_STATUS                    Status;
  EFI_DHCP4_MODE_DATA           Dhcp4ModeData;
  UINT32                        OptionCount;
  EFI_DHCP4_PACKET_OPTION       **OptionList;
  UINT32                        Index;
  ISCSI_SESSION_CONFIG_NVDATA   *NvData;

  Status = Dhcp4->GetModeData (Dhcp4, &Dhcp4ModeData);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  if (Dhcp4ModeData.State != Dhcp4Bound) {
    return EFI_NO_MAPPING;
  }

  NvData = &ConfigData->SessionConfigData;

  CopyMem (&NvData->LocalIp, &Dhcp4ModeData.ClientAddress, sizeof (EFI_IPv4_ADDRESS));
  CopyMem (&NvData->SubnetMask, &Dhcp4ModeData.SubnetMask, sizeof (EFI_IPv4_ADDRESS));
  CopyMem (&NvData->Gateway, &Dhcp4ModeData.RouterAddress, sizeof (EFI_IPv4_ADDRESS));

  OptionCount = 0;
  OptionList  = NULL;

  Status      = Dhcp4->Parse (Dhcp4, Dhcp4ModeData.ReplyPacket, &OptionCount, OptionList);
  if (Status != EFI_BUFFER_TOO_SMALL) {
    return EFI_DEVICE_ERROR;
  }

  OptionList = AllocatePool (OptionCount * sizeof (EFI_DHCP4_PACKET_OPTION *));
  if (OptionList == NULL) {
    return EFI_OUT_OF_RESOURCES;
  }

  Status = Dhcp4->Parse (Dhcp4, Dhcp4ModeData.ReplyPacket, &OptionCount, OptionList);
  if (EFI_ERROR (Status)) {
    FreePool (OptionList);
    return EFI_DEVICE_ERROR;
  }

  for (Index = 0; Index < OptionCount; Index++) {
    //
    // Get DNS server addresses and DHCP server address from this offer.
    //
    if (OptionList[Index]->OpCode == DHCP4_TAG_DNS) {

      if (((OptionList[Index]->Length & 0x3) != 0) || (OptionList[Index]->Length == 0)) {
        Status = EFI_INVALID_PARAMETER;
        break;
      }
      //
      // Primary DNS server address.
      //
      CopyMem (&ConfigData->PrimaryDns, &OptionList[Index]->Data[0], sizeof (EFI_IPv4_ADDRESS));

      if (OptionList[Index]->Length > 4) {
        //
        // Secondary DNS server address.
        //
        CopyMem (&ConfigData->SecondaryDns, &OptionList[Index]->Data[4], sizeof (EFI_IPv4_ADDRESS));
      }
    } else if (OptionList[Index]->OpCode == DHCP4_TAG_SERVER_ID) {
      if (OptionList[Index]->Length != 4) {
        Status = EFI_INVALID_PARAMETER;
        break;
      }

      CopyMem (&ConfigData->DhcpServer, &OptionList[Index]->Data[0], sizeof (EFI_IPv4_ADDRESS));
    }
  }

  FreePool (OptionList);

  return Status;
}


/**
  Parse the DHCP ACK to get the address configuration and DNS information.

  @param[in]       Image            The handle of the driver image.
  @param[in]       Controller       The handle of the controller.
  @param[in, out]  ConfigData       The attempt configuration data.

  @retval EFI_SUCCESS           The DNS information is got from the DHCP ACK.
  @retval EFI_OUT_OF_RESOURCES  Failed to allocate memory.
  @retval EFI_NO_MEDIA          There was a media error.
  @retval Others                Other errors as indicated.

**/
EFI_STATUS
IScsiDoDhcp (
  IN     EFI_HANDLE                  Image,
  IN     EFI_HANDLE                  Controller,
  IN OUT ISCSI_ATTEMPT_CONFIG_NVDATA *ConfigData
  )
{
  EFI_HANDLE                    Dhcp4Handle;
  EFI_DHCP4_PROTOCOL            *Dhcp4;
  EFI_STATUS                    Status;
  EFI_DHCP4_PACKET_OPTION       *ParaList;
  EFI_DHCP4_CONFIG_DATA         Dhcp4ConfigData;
  ISCSI_SESSION_CONFIG_NVDATA   *NvData;
  BOOLEAN                       MediaPresent;

  Dhcp4Handle = NULL;
  Dhcp4       = NULL;
  ParaList    = NULL;

  //
  // Check media status before doing DHCP.
  //
  MediaPresent = TRUE;
  NetLibDetectMedia (Controller, &MediaPresent);
  if (!MediaPresent) {
    return EFI_NO_MEDIA;
  }

  //
  // Create a DHCP4 child instance and get the protocol.
  //
  Status = NetLibCreateServiceChild (
             Controller,
             Image,
             &gEfiDhcp4ServiceBindingProtocolGuid,
             &Dhcp4Handle
             );
  if (EFI_ERROR (Status)) {
    return Status;
  }

  Status = gBS->OpenProtocol (
                  Dhcp4Handle,
                  &gEfiDhcp4ProtocolGuid,
                  (VOID **) &Dhcp4,
                  Image,
                  Controller,
                  EFI_OPEN_PROTOCOL_BY_DRIVER
                  );
  if (EFI_ERROR (Status)) {
    goto ON_EXIT;
  }

  NvData   = &ConfigData->SessionConfigData;

  ParaList = AllocatePool (sizeof (EFI_DHCP4_PACKET_OPTION) + 3);
  if (ParaList == NULL) {
    Status = EFI_OUT_OF_RESOURCES;
    goto ON_EXIT;
  }

  //
  // Ask the server to reply with Netmask, Router, DNS, and RootPath options.
  //
  ParaList->OpCode  = DHCP4_TAG_PARA_LIST;
  ParaList->Length  = (UINT8) (NvData->TargetInfoFromDhcp ? 4 : 3);
  ParaList->Data[0] = DHCP4_TAG_NETMASK;
  ParaList->Data[1] = DHCP4_TAG_ROUTER;
  ParaList->Data[2] = DHCP4_TAG_DNS;
  ParaList->Data[3] = DHCP4_TAG_ROOT_PATH;

  ZeroMem (&Dhcp4ConfigData, sizeof (EFI_DHCP4_CONFIG_DATA));
  Dhcp4ConfigData.OptionCount = 1;
  Dhcp4ConfigData.OptionList  = &ParaList;

  if (NvData->TargetInfoFromDhcp) {
    //
    // Use callback to select an offer that contains target information.
    //
    Dhcp4ConfigData.Dhcp4Callback   = IScsiDhcpSelectOffer;
    Dhcp4ConfigData.CallbackContext = ConfigData;
  }

  Status = Dhcp4->Configure (Dhcp4, &Dhcp4ConfigData);
  if (EFI_ERROR (Status)) {
    goto ON_EXIT;
  }

  Status = Dhcp4->Start (Dhcp4, NULL);
  if (EFI_ERROR (Status)) {
    goto ON_EXIT;
  }
  //
  // Parse the ACK to get required information.
  //
  Status = IScsiParseDhcpAck (Dhcp4, ConfigData);

ON_EXIT:

  if (ParaList != NULL) {
    FreePool (ParaList);
  }

  if (Dhcp4 != NULL) {
    Dhcp4->Stop (Dhcp4);
    Dhcp4->Configure (Dhcp4, NULL);

    gBS->CloseProtocol (
           Dhcp4Handle,
           &gEfiDhcp4ProtocolGuid,
           Image,
           Controller
           );
  }

  NetLibDestroyServiceChild (
    Controller,
    Image,
    &gEfiDhcp4ServiceBindingProtocolGuid,
    Dhcp4Handle
    );

  return Status;
}
