/***********************************************************************
 * 
 *  Copyright (C) 2006 Novell, Inc. All Rights Reserved.
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; version 2.1
 *  of the License.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Library Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, Novell, Inc.
 * 
 *  To contact Novell about this file by physical or electronic mail, 
 *  you may find current contact information at www.novell.com.
 * 
 *  Author: Juan Carlos Luciani <jluciani@novell.com>
 *
 ***********************************************************************/


//===[ Include files ]=====================================================

#include "internal.h"

//===[ Type definitions ]==================================================

#define MAX_RPC_RETRIES 3

//===[ External prototypes ]===============================================

extern
int
SetupOSSLSupport(void);

extern
void
CleanupOSSLSupport(void);

//===[ Function prototypes ]===============================================


//===[ Global variables ]==================================================

static
bool g_rpcInitialized = false;


//++=======================================================================
size_t
CurlWriteCallback(
   IN    void *pData,
   IN    size_t dataItemSz,
   IN    size_t numDataItems,
   IN    RpcSession *pSession)
//
//  Arguments: 
//
//  Returns:   
//
//  Abstract:  
//
//  Notes:
//
// L2
//=======================================================================--
{
   size_t   dataConsumed = numDataItems;

   DbgTrace(1, "-CurlWriteCallback- Start\n", 0);

   // Consume the data by keeping a copy of the data. Note that we may have
   // already consumed some data in which case we need to allocate a new
   // buffer big enough to hold all of it.
   if (pSession->pRecvData == NULL)
   {
      // We have not yet consumed receive data for the current Rpc
      // if the data does not exceed our maximum Rpc reply size.
      if ((numDataItems * dataItemSz) <= MAX_RPC_REPLY_SZ)
      {
         pSession->pRecvData = (char*) malloc(numDataItems * dataItemSz);
         if (pSession->pRecvData)
         {
            // Consume the data
            memcpy(pSession->pRecvData, pData, numDataItems * dataItemSz);
            pSession->recvDataLen = numDataItems * dataItemSz;
         }
         else
         {
            DbgTrace(0, "-CurlWriteCallback- Buffer allocation error\n", 0);
            dataConsumed = CURLE_WRITE_ERROR; // To abort RPC
         }
      }
      else
      {
         DbgTrace(0, "-CurlWriteCallback- Max Rpc reply size exceeded\n", 0);
         dataConsumed = CURLE_WRITE_ERROR; // To abort RPC
      }
   }
   else
   {
      // We have already consumed receive data for the current Rpc, append the new data to it
      // if the data does not exceed our maximum Rpc reply size.
      if ((pSession->recvDataLen + (numDataItems * dataItemSz)) <= MAX_RPC_REPLY_SZ)
      {
         char  *pNewRecvDataBuf = (char*) malloc(pSession->recvDataLen + (numDataItems * dataItemSz));
         if (pNewRecvDataBuf)
         {
            memcpy(pNewRecvDataBuf, pSession->pRecvData, pSession->recvDataLen);
            memcpy(pNewRecvDataBuf + pSession->recvDataLen, pData, numDataItems * dataItemSz);
            pSession->recvDataLen +=  numDataItems * dataItemSz;
            free(pSession->pRecvData);
            pSession->pRecvData = pNewRecvDataBuf;
         }
         else
         {
            DbgTrace(0, "-CurlWriteCallback- Buffer allocation error\n", 0);
            dataConsumed = CURLE_WRITE_ERROR; // To abort RPC

            // Forget about already consumed data
            free(pSession->pRecvData);
            pSession->pRecvData = NULL;
         }
      }
      else
      {
         DbgTrace(0, "-CurlWriteCallback- Max Rpc reply size exceeded\n", 0);
         dataConsumed = CURLE_WRITE_ERROR; // To abort RPC

         // Forget about already consumed data
         free(pSession->pRecvData);
         pSession->pRecvData = NULL;
      }
   }

   DbgTrace(1, "-CurlWriteCallback- End\n", 0);

   return dataConsumed;
}


//++=======================================================================
RpcSession*
OpenRpcSession(
   IN    const char *pHostName,
   IN    const uint16_t hostPort)
//
//  Arguments: 
//
//  Returns:   
//
//  Abstract:  
//
//  Notes:
//
// L2
//=======================================================================--
{
   RpcSession  *pSession = NULL;
   char        *pPartialHttpUrl = NULL;
   char        *pPartialHttpsUrl = NULL;
   int         hostNameLen = strlen(pHostName);

   DbgTrace(1, "-OpenRpcSession- Start\n", 0);

   // Build the partial URL strings that may be used with this session.
   pPartialHttpUrl = (char*) malloc(7 /*"http://"*/ + hostNameLen + 34 /*":XXXX/CasaAuthTokenSvc/Rpc?method="*/ + 1 /*NULL Terminator*/);
   pPartialHttpsUrl = (char*) malloc(8 /*"https://"*/ + hostNameLen + 34 /*":XXXX/CasaAuthTokenSvc/Rpc?method="*/ + 1 /*NULL Terminator*/);
   if (pPartialHttpUrl && pPartialHttpsUrl)
   {
      sprintf(pPartialHttpUrl, "http://%s:%d/CasaAuthTokenSvc/Rpc?method=", pHostName, hostPort);
      sprintf(pPartialHttpsUrl, "https://%s:%d/CasaAuthTokenSvc/Rpc?method=", pHostName, hostPort);

      // Allocate space for the session
      pSession = (RpcSession*) malloc(sizeof(*pSession));
      if (pSession)
      {
         // Zero the session structure
         memset(pSession, 0, sizeof(*pSession));

         // Get a curl handle
         pSession->hCurl = curl_easy_init();
         if (pSession->hCurl != NULL)
         {
            CURLcode result;
            bool setOptError = false;

            // Set necessary options on the handle
            if ((result = curl_easy_setopt(pSession->hCurl, CURLOPT_NOSIGNAL, 0)) != CURLE_OK)
            {
               DbgTrace(0, "-OpenRpcSession- Error setting CURLOPT_NOSIGNAL, code = %d\n", result);
               setOptError = true;
            }

            if ((result = curl_easy_setopt(pSession->hCurl, CURLOPT_USERAGENT, "CASA Client/1.0")) != CURLE_OK)
            {
               DbgTrace(0, "-OpenRpcSession- Error setting CURLOPT_USERAGENT, code = %d\n", result);
               setOptError = true;
            }

            if ((result = curl_easy_setopt(pSession->hCurl, CURLOPT_POST, 1)) != CURLE_OK)
            {
               DbgTrace(0, "-OpenRpcSession- Error setting CURLOPT_POST, code = %d\n", result);
               setOptError = true;
            }

            pSession->headers = curl_slist_append(pSession->headers, "Content-Type: text/html");
            pSession->headers = curl_slist_append(pSession->headers, "Expect:");
            if ((result = curl_easy_setopt(pSession->hCurl, CURLOPT_HTTPHEADER, pSession->headers)) != CURLE_OK)
            {
               DbgTrace(0, "-OpenRpcSession- Error setting CURLOPT_HTTPHEADER, code = %d\n", result);
               setOptError = true;
            }

            if ((result = curl_easy_setopt(pSession->hCurl, CURLOPT_WRITEFUNCTION, CurlWriteCallback)) != CURLE_OK)
            {
               DbgTrace(0, "-OpenRpcSession- Error setting CURLOPT_WRITEFUNCTION, code = %d\n", result);
               setOptError = true;
            }

            if ((result = curl_easy_setopt(pSession->hCurl, CURLOPT_WRITEDATA, pSession)) != CURLE_OK)
            {
               DbgTrace(0, "-OpenRpcSession- Error setting CURLOPT_WRITEDATA, code = %d\n", result);
               setOptError = true;
            }

            // Now check if we succeded
            if (setOptError == false)
            {
               // Success, finish setting up the session object.
               pSession->pPartialHttpUrl = pPartialHttpUrl;
               pSession->partialHttpUrlLen = strlen(pPartialHttpUrl);
               pSession->pPartialHttpsUrl = pPartialHttpsUrl;
               pSession->partialHttpsUrlLen = strlen(pPartialHttpsUrl);

               // Forget about the partial URL buffers so that they do not get deleted below
               pPartialHttpUrl = NULL;
               pPartialHttpsUrl = NULL;
            }
            else
            {
               // Failed to set a needed curl option
               if (pSession->headers)
                  curl_slist_free_all(pSession->headers);

               curl_easy_cleanup(pSession->hCurl);

               free(pSession);
               pSession = NULL;
            }
         }
         else
         {
            DbgTrace(0, "-OpenRpcSession- Error creating curl handle\n", 0);
            free(pSession);
            pSession = NULL;
         }
      }
      else
      {
         DbgTrace(0, "-OpenRpcSession- Failed to allocate buffer for rpc session\n", 0);
      }
   }
   else
   {
      DbgTrace(0, "-OpenRpcSession- Failed to allocate buffer for URL\n", 0);
   }

   // Free buffers not utilized
   if (pPartialHttpUrl)
      free(pPartialHttpUrl);

   if (pPartialHttpsUrl)
      free(pPartialHttpsUrl);

   DbgTrace(2, "-OpenRpcSession- End, pSession = %0lX\n", (long) pSession);

   return pSession;
}


//++=======================================================================
void
CloseRpcSession(
   IN    RpcSession *pSession)
//
//  Arguments: 
//
//  Returns:   
//
//  Abstract:  
//
//  Notes:
//
// L2
//=======================================================================--
{
   DbgTrace(1, "-CloseRpcSession- Start\n", 0);

   // Free any HTTP headers associated with the session
   if (pSession->headers)
      curl_slist_free_all(pSession->headers);

   // Close the curl handle associated with this session
   curl_easy_cleanup(pSession->hCurl);

   // Free the space allocated for the session
   if (pSession->pRecvData)
      free(pSession->pRecvData);

   free(pSession->pPartialHttpUrl);
   free(pSession->pPartialHttpsUrl);

   free(pSession);

   DbgTrace(1, "-CloseRpcSession- End\n", 0);
}


//++=======================================================================
static
CasaStatus
InternalRpc(
   IN    RpcSession *pSession,
   IN    char *pMethod,
   IN    long flags,
   IN    char *pRequestData,
   INOUT char **ppResponseData,
   INOUT size_t *pResponseDataLen)
//
//  Arguments: 
//
//  Returns:   
//
//  Abstract:  
//
//  Notes:
//
// L2
//=======================================================================--
{
#ifndef CASA_STATUS_INVALID_SERVER_CERTIFICATE
#define CASA_STATUS_INVALID_SERVER_CERTIFICATE CASA_STATUS_UNSUCCESSFUL // temporary until casa_status.h is updated
#endif

   CasaStatus  retStatus;
   char        *pPartialUrl;
   int         partialUrlLen;
   char        *pUrl;
   CURLcode    curlResult;

   DbgTrace(1, "-InternalRpc- Start\n", 0);

   // Initialize output parameters
   *ppResponseData = NULL;
   *pResponseDataLen = 0;

   // Setup the URL using the input parameters
   if (flags & SECURE_RPC_FLAG)
   {
      pPartialUrl = pSession->pPartialHttpsUrl;
      partialUrlLen = pSession->partialHttpsUrlLen;

      // Check if we need to ignore invalid CERTS
      if (flags & ALLOW_INVALID_CERTS_RPC_FLAG)
      {
         if ((curlResult = curl_easy_setopt(pSession->hCurl, CURLOPT_SSL_VERIFYPEER, 0)) != CURLE_OK)
         {
            DbgTrace(0, "-InternalRpc- Error setting CURLOPT_SSL_VERIFYPEER, code = %d\n", curlResult);
         }

         if ((curlResult = curl_easy_setopt(pSession->hCurl, CURLOPT_SSL_VERIFYHOST, 0)) != CURLE_OK)
         {
            DbgTrace(0, "-InternalRpc- Error setting CURLOPT_SSL_VERIFYHOST, code = %d\n", curlResult);
         }
      }
      else
      {
         if ((curlResult = curl_easy_setopt(pSession->hCurl, CURLOPT_SSL_VERIFYPEER, 1)) != CURLE_OK)
         {
            DbgTrace(0, "-InternalRpc- Error setting CURLOPT_SSL_VERIFYPEER, code = %d\n", curlResult);
         }

         if ((curlResult = curl_easy_setopt(pSession->hCurl, CURLOPT_SSL_VERIFYHOST, 2)) != CURLE_OK)
         {
            DbgTrace(0, "-InternalRpc- Error setting CURLOPT_SSL_VERIFYHOST, code = %d\n", curlResult);
         }
      }

   }
   else
   {
      pPartialUrl = pSession->pPartialHttpUrl;
      partialUrlLen = pSession->partialHttpUrlLen;
   }

   pUrl = (char*) malloc(partialUrlLen + strlen(pMethod) + 1);
   if (pUrl)
   {
      strcpy(pUrl, pPartialUrl);
      strcat(pUrl, pMethod);

      // Tell curl about the URL
      curlResult = curl_easy_setopt(pSession->hCurl, CURLOPT_URL, pUrl);
      if (curlResult == CURLE_OK)
      {
         // Tell curl about our post data
         curlResult = curl_easy_setopt(pSession->hCurl, CURLOPT_POSTFIELDS, pRequestData);
         if (curlResult == CURLE_OK)
         {
            // Tell curl about our post data len
            curlResult = curl_easy_setopt(pSession->hCurl, CURLOPT_POSTFIELDSIZE, strlen(pRequestData));
            if (curlResult == CURLE_OK)
            {
               // Now do the HTTP request
               curlResult = curl_easy_perform(pSession->hCurl);
               if (curlResult == CURLE_OK)
               {
                  // Get the HTTP Response code
                  long httpCompStatus;
                  curlResult = curl_easy_getinfo(pSession->hCurl, CURLINFO_RESPONSE_CODE, &httpCompStatus);
                  if (curlResult == CURLE_OK)
                  {
                     // Verify that the HTTP request was successfully completed by the server 
                     if (httpCompStatus == 200)
                     {
                        // Success, return the response data to the caller.
                        retStatus = CASA_STATUS_SUCCESS;
                        *ppResponseData = pSession->pRecvData;
                        *pResponseDataLen = pSession->recvDataLen;;

                        // Forget about the response data buffer to keep from freeing it.
                        pSession->pRecvData = NULL;
                     }
                     else
                     {
                        DbgTrace(0, "-InternalRpc- HTTP request did not complete successfully, status = %ld\n", httpCompStatus);
                        retStatus = CasaStatusBuild(CASA_SEVERITY_ERROR,
                                                    CASA_FACILITY_AUTHTOKEN,
                                                    CASA_STATUS_UNSUCCESSFUL);
                     }
                  }
                  else
                  {
                     DbgTrace(0, "-OpenRpcSession- Curl get info failed, code = %d\n", curlResult);
                     retStatus = CasaStatusBuild(CASA_SEVERITY_ERROR,
                                                 CASA_FACILITY_AUTHTOKEN,
                                                 CASA_STATUS_UNSUCCESSFUL);
                  }
               }
               else
               {
                  DbgTrace(0, "-OpenRpcSession- Curl perform failed, code = %d\n", curlResult);
                  retStatus = CasaStatusBuild(CASA_SEVERITY_ERROR,
                                              CASA_FACILITY_AUTHTOKEN,
                                              CASA_STATUS_UNSUCCESSFUL);
               }

               // Make sure that we never exit with a recv data buffer hanging off the session
               if (pSession->pRecvData)
               {
                  free(pSession->pRecvData);
                  pSession->pRecvData = NULL;
               }
            }
            else
            {
               DbgTrace(0, "-OpenRpcSession- Error setting CURLOPT_POSTFIELDSIZE, code = %d\n", curlResult);
               retStatus = CasaStatusBuild(CASA_SEVERITY_ERROR,
                                           CASA_FACILITY_AUTHTOKEN,
                                           CASA_STATUS_UNSUCCESSFUL);
            }
         }
         else
         {
            DbgTrace(0, "-OpenRpcSession- Error setting CURLOPT_POSTFIELDS, code = %d\n", curlResult);
            retStatus = CasaStatusBuild(CASA_SEVERITY_ERROR,
                                        CASA_FACILITY_AUTHTOKEN,
                                        CASA_STATUS_UNSUCCESSFUL);
         }
      }
      else
      {
         DbgTrace(0, "-OpenRpcSession- Error setting CURLOPT_URL, code = %d\n", curlResult);
         retStatus = CasaStatusBuild(CASA_SEVERITY_ERROR,
                                     CASA_FACILITY_AUTHTOKEN,
                                     CASA_STATUS_UNSUCCESSFUL);
      }

      // Free the buffer used to hold the URL
      free(pUrl);
   }
   else
   {
      DbgTrace(0, "-InternalRpc- Buffer allocation failure\n", 0);
      retStatus = CasaStatusBuild(CASA_SEVERITY_ERROR,
                                  CASA_FACILITY_AUTHTOKEN,
                                  CASA_STATUS_INSUFFICIENT_RESOURCES);
   }

   DbgTrace(1, "-InternalRpc- End, retStatus = %0X\n", retStatus);

   return retStatus;
}


//++=======================================================================
CasaStatus
Rpc(
   IN    RpcSession *pSession,
   IN    char *pMethod,
   IN    long flags,
   IN    char *pRequestData,
   INOUT char **ppResponseData,
   INOUT size_t *pResponseDataLen)
//
//  Arguments: 
//
//  Returns:   
//
//  Abstract:  
//
//  Notes:
//
// L2
//=======================================================================--
{
   CasaStatus  retStatus;
   int         retries = 0;

   DbgTrace(1, "-Rpc- Start\n", 0);

   // Retry the RPC as needed
   do
   {
      // Issue the RPC
      retStatus = InternalRpc(pSession,
                              pMethod,
                              flags,
                              pRequestData,
                              ppResponseData,
                              pResponseDataLen);

      // Account for this try
      retries ++;

   } while (CasaStatusCode(retStatus) == CASA_STATUS_AUTH_SERVER_UNAVAILABLE
            && retries < MAX_RPC_RETRIES);

   DbgTrace(1, "-Rpc- End, retStatus = %0X\n", retStatus);

   return retStatus;
}


//++=======================================================================
CasaStatus
InitializeRpc(void)
//
//  Arguments: 
//
//  Returns:   
//
//  Abstract:  
//
//  Notes:
//
// L2
//=======================================================================--
{
   CasaStatus  retStatus = CasaStatusBuild(CASA_SEVERITY_ERROR,
                                           CASA_FACILITY_AUTHTOKEN,
                                           CASA_STATUS_UNSUCCESSFUL);

   DbgTrace(1, "-InitializeRpc- Start\n", 0);

   // Initialize OpenSSL support
   if (SetupOSSLSupport() == 0)
   {
      // Perform libcurl initializatoin
      CURLcode curlStatus = curl_global_init(CURL_GLOBAL_SSL);
      if (curlStatus != 0)
      {
         DbgTrace(0, "-InitializeRpc- Error initializing libcurl, curlStatus = %0X\n", curlStatus);
         CleanupOSSLSupport();
      }
      else
      {
         // Success
         g_rpcInitialized = true;
         retStatus = CASA_STATUS_SUCCESS;
      }
   }
   else
   {
      DbgTrace(0, "-InitializeRpc- OpenSSL support setup failure\n", 0);
   }

   DbgTrace(1, "-InitializeRpc- End, retStatus = %0X\n", retStatus);

   return retStatus;
}


//++=======================================================================
void
UnInitializeRpc(void)
//
//  Arguments: 
//
//  Returns:   
//
//  Abstract:  
//
//  Notes:
//
// L2
//=======================================================================--
{
   DbgTrace(1, "-UnInitializeRpc- Start\n", 0);

   // Only try to cleanup if we were initialized
   if (g_rpcInitialized)
   {
      // Cleanup libcurl
      curl_global_cleanup();

      // Cleanup OpenSSL support
      CleanupOSSLSupport();

      // Forget about having been initialized
      g_rpcInitialized = false;
   }

   DbgTrace(1, "-UnInitializeRpc- End\n", 0);
}


//++=======================================================================
//++=======================================================================
//++=======================================================================