/*
 * FreeRTOS Greengrass V2.0.1
 * Copyright (C) 2020 Amazon.com, Inc. or its affiliates.  All Rights Reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 * http://aws.amazon.com/freertos
 * http://www.FreeRTOS.org
 */

/**
 * @file aws_helper_secure_connect.c
 * @brief Secure connection wrapper
 *
 * API used by green grass discovery to help establishing
 * secure connect with a given host
 */

/* SOCKETS includes. */
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "semphr.h"

/* Helper interface includes. */
#include "aws_helper_secure_connect.h"
#include "aws_ggd_config.h"
#include "aws_ggd_config_defaults.h"

/* Standard includes. */
#include <string.h>

#define helperMAX_IP_ADDRESS_OCTETS    4u

/**
 * @brief This function return non 0 if it is an IP and 0 if it isn't
 */
static uint32_t prvIsIPaddress( const char * pcIPAddress );

/*-----------------------------------------------------------*/

BaseType_t GGD_SecureConnect_Connect( const GGD_HostAddressData_t * pxHostAddressData,
                                      Socket_t * pxSocket,
                                      uint32_t ulReceiveTimeOut,
                                      uint32_t ulSendTimeOut )
{
    const TickType_t xReceiveTimeOut = pdMS_TO_TICKS( ulReceiveTimeOut );
    const TickType_t xSendTimeOut = pdMS_TO_TICKS( ulSendTimeOut );
    SocketsSockaddr_t xServerAddress;
    size_t xURLLength;
    BaseType_t xIsIPAddress;
    BaseType_t xStatus;

    configASSERT( pxHostAddressData != NULL );
    configASSERT( pxSocket != NULL );

    /* Calculate the length of the supplied URL. */
    xURLLength = strlen( pxHostAddressData->pcHostAddress );

    /* Ensure that the length of the specified URL is
     * within the permitted limits. */
    if( xURLLength <= ( size_t ) securesocketsMAX_DNS_NAME_LENGTH )
    {
        /* Create the socket. */
        *pxSocket = SOCKETS_Socket( SOCKETS_AF_INET,
                                    SOCKETS_SOCK_STREAM,
                                    SOCKETS_IPPROTO_TCP );

        if( *pxSocket == SOCKETS_INVALID_SOCKET )
        {
            xStatus = pdFAIL;
        }
        else
        {
            xStatus = pdPASS;
        }

        if( xStatus == pdPASS )
        {
            if( prvIsIPaddress( pxHostAddressData->pcHostAddress ) == ( uint32_t ) 0 )
            {
                xIsIPAddress = pdFALSE;
            }
            else
            {
                xIsIPAddress = pdTRUE;
            }

            xServerAddress.ucLength = sizeof( SocketsSockaddr_t );
            xServerAddress.usPort = SOCKETS_htons( pxHostAddressData->usPort );
            xServerAddress.ulAddress =
                SOCKETS_GetHostByName( pxHostAddressData->pcHostAddress );
            xServerAddress.ucSocketDomain = SOCKETS_AF_INET;

            /* Set send timeout for the socket. */
            ( void ) SOCKETS_SetSockOpt( *pxSocket,
                                         0,
                                         SOCKETS_SO_SNDTIMEO,
                                         &xSendTimeOut,
                                         sizeof( xSendTimeOut ) );

            /* Set receive timeout for the socket. */
            ( void ) SOCKETS_SetSockOpt( *pxSocket,
                                         0,
                                         SOCKETS_SO_RCVTIMEO,
                                         &xReceiveTimeOut,
                                         sizeof( xReceiveTimeOut ) );

            /* Set secure connection. */
            ( void ) SOCKETS_SetSockOpt( *pxSocket,
                                         0,
                                         SOCKETS_SO_REQUIRE_TLS,
                                         NULL,
                                         ( size_t ) 0 );

            if( pxHostAddressData->pcCertificate != NULL )
            {
                if( SOCKETS_SetSockOpt( *pxSocket,
                                        0,
                                        SOCKETS_SO_TRUSTED_SERVER_CERTIFICATE,
                                        pxHostAddressData->pcCertificate,
                                        ( size_t ) pxHostAddressData->ulCertificateSize )
                    != SOCKETS_ERROR_NONE )
                {
                    xStatus = pdFAIL;
                }
            }

            if( xIsIPAddress == pdFALSE )
            {
                if( SOCKETS_SetSockOpt( *pxSocket,
                                        0,
                                        SOCKETS_SO_SERVER_NAME_INDICATION,
                                        pxHostAddressData->pcHostAddress,
                                        ( size_t ) 1 + xURLLength )
                    != SOCKETS_ERROR_NONE )
                {
                    xStatus = pdFAIL;
                }
            }

            /* Establish the TCP connection. */
            if( pdPASS == xStatus )
            {
                if( ( SOCKETS_Connect( *pxSocket,
                                       &xServerAddress,
                                       ( uint32_t ) sizeof( xServerAddress ) )
                      != SOCKETS_ERROR_NONE ) )
                {
                    GGD_SecureConnect_Disconnect( pxSocket );
                    xStatus = pdFAIL;
                }
            }
        }
    }
    else
    {
        ggdconfigPRINT( "Malformed URL\r\n" );
        xStatus = pdFAIL;
    }

    return xStatus;
}
/*-----------------------------------------------------------*/

void GGD_SecureConnect_Disconnect( Socket_t * pxSocket )
{
    const TickType_t xShortDelay = pdMS_TO_TICKS( 10 );
    TickType_t xTicksToWait = xShortDelay * ( TickType_t ) 100;
    TimeOut_t xTimeOut;
    char cBuf; /*lint !e971 can use char without signed/unsigned. */

    configASSERT( pxSocket != NULL );
    configASSERT( *pxSocket != SOCKETS_INVALID_SOCKET );
    ggdconfigPRINT( "About to close socket.\r\n" );

    /* Initialize xTimeOut.  This records the time at which this function was
     * entered. */
    vTaskSetTimeOutState( &xTimeOut );

    /* Shutdown the connection. */
    ( void ) SOCKETS_Shutdown( *pxSocket, ( uint32_t ) SOCKETS_SHUT_RDWR );

    /* Wait for the socket to disconnect gracefully (indicated by a
     * SOCKETS_EINVAL error) before closing the socket. */
    while( SOCKETS_Recv( *pxSocket, &cBuf, sizeof( cBuf ), ( uint32_t ) 0 ) >= 0 )
    {
        vTaskDelay( xShortDelay );

        if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) != pdFALSE )
        {
            /* Timed out before the wanted number of bytes were available, exit
             * the loop. */
            break;
        }
    }

    /* Close the socket. */
    ( void ) SOCKETS_Close( *pxSocket );
    *pxSocket = SOCKETS_INVALID_SOCKET;
    ggdconfigPRINT( "Socket closed.\r\n" );

    #if ( INCLUDE_uxTaskGetStackHighWaterMark == 1 )
        {
            configPRINTF( ( "Stack high watermark for discovery helper task: %u.\r\n",
                            uxTaskGetStackHighWaterMark( NULL ) ) );
        }
    #endif
}
/*-----------------------------------------------------------*/


BaseType_t GGD_SecureConnect_Send( const char * pcPayload, /*lint !e971 can use char without signed/unsigned. */
                                   const uint32_t ulPayloadSize,
                                   const Socket_t xSocket )
{
    BaseType_t xStatus = pdFAIL;
    uint32_t ulBytesSent = 0;
    int32_t lSendRetVal;

    configASSERT( pcPayload != NULL );

    /* Keep re-trying until timeout or any error
     * other than SOCKETS_EWOULDBLOCK occurs. */

    if( ulPayloadSize > 0 )
    {
        while( ulBytesSent < ulPayloadSize )
        {
            /* Try sending the remaining data. */
            lSendRetVal = SOCKETS_Send( xSocket,
                                        ( const unsigned char * ) &pcPayload[ ulBytesSent ],
                                        ( size_t ) ( ulPayloadSize - ulBytesSent ),
                                        ( uint32_t ) 0 );

            /* A negative return value from SOCKETS_Send
             * means some error occurred. */
            if( lSendRetVal < 0 )
            {
                break;
            }
            else
            {
                /* Update the count of sent bytes. */
                ulBytesSent += ( uint32_t ) lSendRetVal;
            }
        }

        if( ulBytesSent == ulPayloadSize )
        {
            xStatus = pdPASS;
        }
        else
        {
            ggdconfigPRINT( "SecureConnect - error sending secure data to network\r\n" );
            xStatus = pdFAIL;
        }
    }
    else
    {
        xStatus = pdPASS;
    }

    return xStatus;
}
/*-----------------------------------------------------------*/

BaseType_t GGD_SecureConnect_Read( char * pcBuffer, /*lint !e971 can use char without signed/unsigned. */
                                   const uint32_t ulBufferSize,
                                   const Socket_t xSocket,
                                   uint32_t * pulDataRecvSize )
{
    int32_t lTmpStatus = 0;
    BaseType_t xStatus = pdFAIL;
    uint16_t usNbRetry;

    configASSERT( pulDataRecvSize != NULL );
    configASSERT( pcBuffer != NULL );

    for( usNbRetry = 0; usNbRetry < ( uint16_t ) ggdconfigTCP_RECEIVE_RETRY; usNbRetry++ )
    {
        lTmpStatus = SOCKETS_Recv( xSocket, ( unsigned char * ) pcBuffer,
                                   ( uint32_t ) ulBufferSize,
                                   ( uint32_t ) 0 );

        /* Check if it is a Timeout. */
        if( lTmpStatus != 0 )
        {
            if( lTmpStatus < 0 )
            {
                ggdconfigPRINT( "SecureConnect - recv error, %d\r\n", lTmpStatus );

                xStatus = pdFAIL;
            }
            else
            {
                xStatus = pdPASS;
            }

            /* If it is not a timeout, break. */
            break;
        }
        else
        {
            /* It is a timeout, retry. */
            ggdconfigPRINT( "SecureConnect - recv Timeout\r\n" );
        }
    }

    if( usNbRetry == ( uint16_t ) ggdconfigTCP_RECEIVE_RETRY )
    {
        ggdconfigPRINT( "SecureConnect - recv number of Timeout exceeded\r\n" );
        xStatus = pdFAIL;
    }

    *pulDataRecvSize = ( uint32_t ) lTmpStatus;

    return xStatus;
}
/*-----------------------------------------------------------*/


static uint32_t prvIsIPaddress( const char * pcIPAddress )
{
    const uint32_t ulDecimalBase = 10u;
    uint8_t ucOctet[ helperMAX_IP_ADDRESS_OCTETS ];
    const char * pcPointerOnEntering;
    uint32_t ulReturn = 0UL, ulValue;
    UBaseType_t uxOctetNumber;
    BaseType_t xResult = pdPASS;

    for( uxOctetNumber = 0u; uxOctetNumber < helperMAX_IP_ADDRESS_OCTETS; uxOctetNumber++ )
    {
        ulValue = 0UL;
        pcPointerOnEntering = pcIPAddress;

        while( ( *pcIPAddress >= '0' ) && ( *pcIPAddress <= '9' ) )
        {
            /* Move previous read characters into the next decimal
             * position. */
            ulValue *= ulDecimalBase;

            /* Add the binary value of the ascii character. */
            ulValue += ( ( uint32_t ) ( *pcIPAddress ) - ( uint32_t ) '0' );

            /* Move to next character in the string. */
            pcIPAddress++;
        }

        /* Check characters were read. */
        if( pcIPAddress == pcPointerOnEntering )
        {
            xResult = pdFAIL;
        }

        /* Check the value fits in an 8-bit number. */
        if( ulValue > 0xffUL )
        {
            xResult = pdFAIL;
        }
        else
        {
            ucOctet[ uxOctetNumber ] = ( uint8_t ) ulValue;

            /* Check the next character is as expected. */
            if( uxOctetNumber < ( helperMAX_IP_ADDRESS_OCTETS - 1u ) )
            {
                if( *pcIPAddress != '.' )
                {
                    xResult = pdFAIL;
                }
                else
                {
                    /* Move past the dot. */
                    pcIPAddress++;
                }
            }
        }

        if( xResult == pdFAIL )
        {
            /* No point going on. */
            break;
        }
    }

    if( *pcIPAddress != ( char ) 0 )
    {
        /* Expected the end of the string. */
        xResult = pdFAIL;
    }

    if( uxOctetNumber != helperMAX_IP_ADDRESS_OCTETS )
    {
        /* Didn't read enough octets. */
        xResult = pdFAIL;
    }

    if( xResult == pdPASS )
    {
        ulReturn = SOCKETS_inet_addr_quick( ucOctet[ 0 ], ucOctet[ 1 ], ucOctet[ 2 ], ucOctet[ 3 ] );
    }

    return ulReturn;
}
