/*
 * FreeRTOS Secure Sockets V1.1.9
 * 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 iot_secure_sockets.c
 * @brief WiFi and Secure Socket interface implementation.
 */

/* Define _SECURE_SOCKETS_WRAPPER_NOT_REDEFINE to prevent secure sockets functions
 * from redefining in iot_secure_sockets_wrapper_metrics.h */
#define _SECURE_SOCKETS_WRAPPER_NOT_REDEFINE

/* Socket and WiFi interface includes. */
#include "iot_secure_sockets.h"


#include "lwip/sockets.h"
#include "lwip/netdb.h"

#include "iot_wifi.h"

#include "iot_tls.h"

#include "FreeRTOSConfig.h"

#include "task.h"

#include <stdbool.h>

#undef _SECURE_SOCKETS_WRAPPER_NOT_REDEFINE

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

#define SS_STATUS_CONNECTED    ( 1 )
#define SS_STATUS_SECURED      ( 2 )

/*
 * secure socket context.
 */
typedef enum E_AWS_SOCkET_RX_STATE
{
    SST_RX_IDLE,
    SST_RX_READY,
    SST_RX_CLOSING,
    SST_RX_CLOSED,
} T_AWS_SOCKET_RX_STATE;

typedef struct _ss_ctx_t
{
    int ip_socket;

    int state;
    unsigned int status;
    int send_flag;
    int recv_flag;

    TaskHandle_t rx_handle;
    void ( * rx_callback )( Socket_t pxSocket );

    bool enforce_tls;
    void * tls_ctx;
    char * destination;

    char * server_cert;
    int server_cert_len;

    char ** ppcAlpnProtocols;
    uint32_t ulAlpnProtocolsCount;
} ss_ctx_t;

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

/*#define SUPPORTED_DESCRIPTORS  (2) */

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

/*static int8_t sockets_allocated = SUPPORTED_DESCRIPTORS; */
static int8_t sockets_allocated = socketsconfigDEFAULT_MAX_NUM_SECURE_SOCKETS;


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

/*
 * convert from system ticks to seconds.
 */
#define TICK_TO_S( _t_ )     ( ( _t_ ) / configTICK_RATE_HZ )

/*
 * convert from system ticks to micro seconds.
 */
#define TICK_TO_US( _t_ )    ( ( _t_ ) * 1000 / configTICK_RATE_HZ * 1000 )

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

/*
 * @brief Network send callback.
 */
static BaseType_t prvNetworkSend( void * pvContext,
                                  const unsigned char * pucData,
                                  size_t xDataLength )
{
    ss_ctx_t * ctx = ( ss_ctx_t * ) pvContext;

    int ret = lwip_send( ctx->ip_socket,
                         pucData,
                         xDataLength,
                         ctx->send_flag );

    return ( BaseType_t ) ret;
}

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

/*
 * @brief Network receive callback.
 */
static BaseType_t prvNetworkRecv( void * pvContext,
                                  unsigned char * pucReceiveBuffer,
                                  size_t xReceiveLength )
{
    ss_ctx_t * ctx;

    ctx = ( ss_ctx_t * ) pvContext;

    if( 0 > ctx->ip_socket )
    {
        return SOCKETS_SOCKET_ERROR;
    }

    int ret = lwip_recv( ctx->ip_socket,
                         pucReceiveBuffer,
                         xReceiveLength,
                         ctx->recv_flag );

    if( -1 == ret )
    {
        /*
         * 1. EWOULDBLOCK if the socket is NON-blocking, but there is no data
         *    when recv is called.
         * 2. EAGAIN if the socket would block and have waited long enough but
         *    packet is not received.
         */
        if( ( errno == EWOULDBLOCK ) || ( errno == EAGAIN ) )
        {
            return SOCKETS_ERROR_NONE; /* timeout or would block */
        }

        /*
         * socket is not connected.
         */
        if( errno == EBADF )
        {
            return SOCKETS_ECLOSED;
        }
    }

    if( ( 0 == ret ) && ( errno == ENOTCONN ) )
    {
        ret = SOCKETS_ECLOSED;
    }

    return ( BaseType_t ) ret;
}

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

static void vTaskRxSelect( void * param )
{
    ss_ctx_t * ctx = ( ss_ctx_t * ) param;
    int s = ctx->ip_socket;

    fd_set read_fds;
    fd_set write_fds;
    fd_set err_fds;

    FD_ZERO( &read_fds );
    FD_ZERO( &write_fds );
    FD_ZERO( &err_fds );

    FD_SET( s, &read_fds );
    FD_SET( s, &err_fds );

    ctx->state = SST_RX_READY;

    while( 1 )
    {
        if( ctx->state == SST_RX_CLOSING )
        {
            ctx->state = SST_RX_CLOSED;
            vTaskDelete( NULL );
        }

        if( lwip_select( s + 1, &read_fds, &write_fds, &err_fds, NULL ) == -1 )
        {
            /*TaskHandle_t rx_handle = ctx->rx_handle; */

            /*ctx->rx_handle   = NULL; */
            /*ctx->rx_callback = NULL; */

            /*vTaskDelete( rx_handle ); */
            vTaskDelete( NULL );
        }

        if( FD_ISSET( s, &read_fds ) )
        {
            configASSERT( ctx->rx_callback );
            ctx->rx_callback( ( Socket_t ) ctx );
            /*vTaskDelay( 10 ); // delay a little bit to yield time for RX */
        }
    }
}

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

static void prvRxSelectSet( ss_ctx_t * ctx,
                            const void * pvOptionValue )
{
    BaseType_t xReturned;
    TaskHandle_t xHandle = NULL;
    configSTACK_DEPTH_TYPE xStackDepth = socketsconfigRECEIVE_CALLBACK_TASK_STACK_DEPTH;

    ctx->rx_callback = ( void ( * )( Socket_t ) )pvOptionValue;

    xReturned = xTaskCreate( vTaskRxSelect, /* pvTaskCode */
                             "rxs",         /* pcName */
                             xStackDepth,   /* usStackDepth */
                             ctx,           /* pvParameters */
                             1,             /* uxPriority */
                             &xHandle );    /* pxCreatedTask */

    configASSERT( xReturned == pdPASS );
    configASSERT( xHandle != NULL );

    ctx->rx_handle = xHandle;
}

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

static void prvRxSelectClear( ss_ctx_t * ctx )
{
    /* TODO */
}

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

Socket_t SOCKETS_Socket( int32_t lDomain,
                         int32_t lType,
                         int32_t lProtocol )
{
    ss_ctx_t * ctx;

    configASSERT( lDomain == SOCKETS_AF_INET );
    configASSERT( lType == SOCKETS_SOCK_STREAM );
    configASSERT( lProtocol == SOCKETS_IPPROTO_TCP );

    if( ( lDomain != SOCKETS_AF_INET ) ||
        ( lType != SOCKETS_SOCK_STREAM ) ||
        ( lProtocol != SOCKETS_IPPROTO_TCP ) ||
        ( sockets_allocated <= 0 )
        )
    {
        return SOCKETS_INVALID_SOCKET;
    }

    ctx = ( ss_ctx_t * ) pvPortMalloc( sizeof( *ctx ) );

    if( ctx )
    {
        memset( ctx, 0, sizeof( *ctx ) );

        ctx->ip_socket = lwip_socket( lDomain, lType, lProtocol );

        if( ctx->ip_socket >= 0 )
        {
            sockets_allocated--;
            return ( Socket_t ) ctx;
        }

        vPortFree( ctx );
    }

    return ( Socket_t ) SOCKETS_INVALID_SOCKET;
}

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

int32_t SOCKETS_Connect( Socket_t xSocket,
                         SocketsSockaddr_t * pxAddress,
                         Socklen_t xAddressLength )
{
    ss_ctx_t * ctx;

    if( SOCKETS_INVALID_SOCKET == xSocket )
    {
        return SOCKETS_EINVAL;
    }

    /* removed because qualification program wants invalid length to go through */
    #if 0
        if( ( NULL == pxAddress ) || ( 0 == xAddressLength ) )
        {
            return SOCKETS_EINVAL;
        }
    #endif

    if( pxAddress == NULL )
    {
        return SOCKETS_EINVAL;
    }

    /* support only SOCKETS_AF_INET for now */
    pxAddress->ucSocketDomain = SOCKETS_AF_INET;

    ctx = ( ss_ctx_t * ) xSocket;

    if( 0 <= ctx->ip_socket )
    {
        struct sockaddr_in sa_addr = { 0 };
        int ret;

        sa_addr.sin_family = pxAddress->ucSocketDomain ? pxAddress->ucSocketDomain : AF_INET;
        sa_addr.sin_addr.s_addr = pxAddress->ulAddress;
        sa_addr.sin_port = pxAddress->usPort;

        ret = lwip_connect( ctx->ip_socket,
                            ( struct sockaddr * ) &sa_addr,
                            sizeof( sa_addr ) );

        if( 0 == ret )
        {
            TLSParams_t tls_params = { 0 };
            BaseType_t status;

            ctx->status |= SS_STATUS_CONNECTED;

            if( !ctx->enforce_tls )
            {
                return SOCKETS_ERROR_NONE;
            }

            tls_params.ulSize = sizeof( tls_params );
            tls_params.pcDestination = ctx->destination;
            tls_params.pcServerCertificate = ctx->server_cert;
            tls_params.ulServerCertificateLength = ctx->server_cert_len;
            tls_params.pvCallerContext = ctx;
            tls_params.pxNetworkRecv = prvNetworkRecv;
            tls_params.pxNetworkSend = prvNetworkSend;
            tls_params.ppcAlpnProtocols = ( const char ** ) ctx->ppcAlpnProtocols;
            tls_params.ulAlpnProtocolsCount = ctx->ulAlpnProtocolsCount;

            status = TLS_Init( &ctx->tls_ctx, &tls_params );

            if( pdFREERTOS_ERRNO_NONE != status )
            {
                configPRINTF( ( "TLS_Init fail\n" ) );
                return SOCKETS_SOCKET_ERROR;
            }

            status = TLS_Connect( ctx->tls_ctx );

            if( pdFREERTOS_ERRNO_NONE == status )
            {
                ctx->status |= SS_STATUS_SECURED;
                return SOCKETS_ERROR_NONE;
            }
            else
            {
                configPRINTF( ( "TLS_Connect fail (0x%x, %s)\n", ( unsigned int ) -status, ctx->destination ? ctx->destination : "NULL" ) );
            }
        }
        else
        {
            configPRINTF( ( "LwIP connect fail %d %d\n", ret, errno ) );
        }
    }
    else
    {
        configPRINTF( ( "Invalid ip socket\n" ) );
    }

    return SOCKETS_SOCKET_ERROR;
}

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

int32_t SOCKETS_Recv( Socket_t xSocket,
                      void * pvBuffer,
                      size_t xBufferLength,
                      uint32_t ulFlags )
{
    ss_ctx_t * ctx = ( ss_ctx_t * ) xSocket;

    if( ( ctx->status & SS_STATUS_CONNECTED ) != SS_STATUS_CONNECTED )
    {
        return SOCKETS_ENOTCONN;
    }

    if( SOCKETS_INVALID_SOCKET == xSocket )
    {
        return SOCKETS_SOCKET_ERROR;
    }

    if( ( NULL == pvBuffer ) || ( 0 == xBufferLength ) )
    {
        return SOCKETS_EINVAL;
    }

    ctx->recv_flag = ulFlags;

    if( 0 > ctx->ip_socket )
    {
        return SOCKETS_SOCKET_ERROR;
    }

    if( ctx->enforce_tls )
    {
        /* Receive through TLS pipe, if negotiated. */
        return TLS_Recv( ctx->tls_ctx, pvBuffer, xBufferLength );
    }
    else
    {
        return prvNetworkRecv( ( void * ) ctx, pvBuffer, xBufferLength );
    }
}

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

int32_t SOCKETS_Send( Socket_t xSocket,
                      const void * pvBuffer,
                      size_t xDataLength,
                      uint32_t ulFlags )
{
    ss_ctx_t * ctx;

    if( SOCKETS_INVALID_SOCKET == xSocket )
    {
        return SOCKETS_SOCKET_ERROR;
    }

    if( ( NULL == pvBuffer ) || ( 0 == xDataLength ) )
    {
        return SOCKETS_EINVAL;
    }

    ctx = ( ss_ctx_t * ) xSocket;
    ctx->send_flag = ulFlags;

    if( 0 > ctx->ip_socket )
    {
        return SOCKETS_SOCKET_ERROR;
    }

    if( ctx->enforce_tls )
    {
        /* Send through TLS pipe, if negotiated. */
        return TLS_Send( ctx->tls_ctx, pvBuffer, xDataLength );
    }
    else
    {
        return prvNetworkSend( ( void * ) ctx, pvBuffer, xDataLength );
    }
}

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

int32_t SOCKETS_Shutdown( Socket_t xSocket,
                          uint32_t ulHow )
{
    ss_ctx_t * ctx;
    int ret;

    if( SOCKETS_INVALID_SOCKET == xSocket )
    {
        return SOCKETS_EINVAL;
    }

    ctx = ( ss_ctx_t * ) xSocket;

    if( 0 > ctx->ip_socket )
    {
        return SOCKETS_SOCKET_ERROR;
    }

    ret = lwip_shutdown( ctx->ip_socket, ( int ) ulHow );

    if( 0 > ret )
    {
        return SOCKETS_SOCKET_ERROR;
    }

    return SOCKETS_ERROR_NONE;
}

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

int32_t SOCKETS_Close( Socket_t xSocket )
{
    ss_ctx_t * ctx;

    uint32_t ulProtocol;

    if( SOCKETS_INVALID_SOCKET == xSocket )
    {
        return SOCKETS_EINVAL;
    }

    ctx = ( ss_ctx_t * ) xSocket;

    /* Clean-up application protocol array. */
    if( NULL != ctx->ppcAlpnProtocols )
    {
        for( ulProtocol = 0;
             ulProtocol < ctx->ulAlpnProtocolsCount;
             ulProtocol++ )
        {
            if( NULL != ctx->ppcAlpnProtocols[ ulProtocol ] )
            {
                vPortFree( ctx->ppcAlpnProtocols[ ulProtocol ] );
            }
        }

        vPortFree( ctx->ppcAlpnProtocols );
    }

    if( true == ctx->enforce_tls )
    {
        TLS_Cleanup( ctx->tls_ctx );
    }

    if( 0 <= ctx->ip_socket )
    {
        int cnt = 0;
        ctx->state = SST_RX_CLOSING;

        while( ( ctx->state != SST_RX_CLOSED ) && ( cnt < 30 ) )
        {
            cnt++;
            vTaskDelay( 10 );
        }

        lwip_close( ctx->ip_socket );

        sockets_allocated++;
    }

    if( ctx->server_cert )
    {
        vPortFree( ctx->server_cert );
    }

    if( ctx->destination )
    {
        vPortFree( ctx->destination );
    }

    vPortFree( ctx );

    return SOCKETS_ERROR_NONE;
}

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

int32_t SOCKETS_SetSockOpt( Socket_t xSocket,
                            int32_t lLevel,
                            int32_t lOptionName,
                            const void * pvOptionValue,
                            size_t xOptionLength )
{
    ss_ctx_t * ctx;
    int ret;
    char ** ppcAlpnIn = ( char ** ) pvOptionValue;
    size_t xLength = 0;
    uint32_t ulProtocol;

    if( SOCKETS_INVALID_SOCKET == xSocket )
    {
        return SOCKETS_EINVAL;
    }

    ctx = ( ss_ctx_t * ) xSocket;

    if( 0 > ctx->ip_socket )
    {
        return SOCKETS_SOCKET_ERROR;
    }

    switch( lOptionName )
    {
        case SOCKETS_SO_RCVTIMEO:
        case SOCKETS_SO_SNDTIMEO:
           {
               TickType_t ticks;
               struct timeval tv;

               ticks = *( ( const TickType_t * ) pvOptionValue );

               tv.tv_sec = TICK_TO_S( ticks );
               tv.tv_usec = TICK_TO_US( ticks % configTICK_RATE_HZ );

               ret = lwip_setsockopt( ctx->ip_socket,
                                      SOL_SOCKET,
                                      lOptionName == SOCKETS_SO_RCVTIMEO ? SO_RCVTIMEO : SO_SNDTIMEO,
                                      ( struct timeval * ) &tv,
                                      sizeof( tv ) );

               if( 0 != ret )
               {
                   return SOCKETS_EINVAL;
               }

               break;
           }

        case SOCKETS_SO_NONBLOCK:
           {
               int opt;

               if( ( ctx->status & SS_STATUS_CONNECTED ) != SS_STATUS_CONNECTED )
               {
                   return SOCKETS_ENOTCONN;
               }

               opt = 1;

               ret = lwip_ioctl( ctx->ip_socket, FIONBIO, &opt );

               if( 0 != ret )
               {
                   return SOCKETS_EINVAL;
               }

               break;
           }

        case SOCKETS_SO_REQUIRE_TLS:

            if( ctx->status & SS_STATUS_CONNECTED )
            {
                return SOCKETS_EISCONN;
            }

            ctx->enforce_tls = true;
            break;

        case SOCKETS_SO_TRUSTED_SERVER_CERTIFICATE:

            if( ctx->status & SS_STATUS_CONNECTED )
            {
                return SOCKETS_EISCONN;
            }

            if( ( NULL == pvOptionValue ) || ( 0 == xOptionLength ) )
            {
                return SOCKETS_EINVAL;
            }

            if( ctx->server_cert )
            {
                vPortFree( ctx->server_cert );
            }

            ctx->server_cert = pvPortMalloc( xOptionLength + 1 );

            if( NULL == ctx->server_cert )
            {
                return SOCKETS_ENOMEM;
            }

            memset( ctx->server_cert, 0, xOptionLength + 1 );
            memcpy( ctx->server_cert, pvOptionValue, xOptionLength );
            ctx->server_cert_len = xOptionLength;

            break;

        case SOCKETS_SO_SERVER_NAME_INDICATION:

            if( ctx->status & SS_STATUS_CONNECTED )
            {
                return SOCKETS_EISCONN;
            }

            if( ( NULL == pvOptionValue ) || ( 0 == xOptionLength ) )
            {
                return SOCKETS_EINVAL;
            }

            if( ctx->destination )
            {
                vPortFree( ctx->destination );
            }

            ctx->destination = pvPortMalloc( xOptionLength + 1 );

            if( NULL == ctx->destination )
            {
                return SOCKETS_ENOMEM;
            }

            memcpy( ctx->destination, pvOptionValue, xOptionLength );
            ctx->destination[ xOptionLength ] = '\0';

            break;

        case SOCKETS_SO_WAKEUP_CALLBACK:

            if( ( xOptionLength == sizeof( void * ) ) &&
                ( pvOptionValue != NULL ) )
            {
                prvRxSelectSet( ctx, pvOptionValue );
            }
            else
            {
                prvRxSelectClear( ctx );
            }

            break;

        case SOCKETS_SO_ALPN_PROTOCOLS:

            /* Do not set the ALPN option if the socket is already connected. */
            if( ctx->status & SS_STATUS_CONNECTED )
            {
                return SOCKETS_EISCONN;
            }

            /* Allocate a sufficiently long array of pointers. */
            ctx->ulAlpnProtocolsCount = 1 + xOptionLength;

            if( NULL == ( ctx->ppcAlpnProtocols =
                              ( char ** ) pvPortMalloc( ctx->ulAlpnProtocolsCount * sizeof( char * ) ) ) )
            {
                return SOCKETS_ENOMEM;
            }
            else
            {
                ctx->ppcAlpnProtocols[
                    ctx->ulAlpnProtocolsCount - 1 ] = NULL;
            }

            /* Copy each protocol string. */
            for( ulProtocol = 0; ( ulProtocol < ctx->ulAlpnProtocolsCount - 1 ); ulProtocol++ )
            {
                xLength = strlen( ppcAlpnIn[ ulProtocol ] );

                if( NULL == ( ctx->ppcAlpnProtocols[ ulProtocol ] =
                                  ( char * ) pvPortMalloc( 1 + xLength ) ) )
                {
                    return SOCKETS_ENOMEM;
                }
                else
                {
                    memcpy( ctx->ppcAlpnProtocols[ ulProtocol ],
                            ppcAlpnIn[ ulProtocol ],
                            xLength );
                    ctx->ppcAlpnProtocols[ ulProtocol ][ xLength ] = '\0';
                }
            }

            break;

        default:
            return SOCKETS_ENOPROTOOPT;
    }

    return SOCKETS_ERROR_NONE;
}

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

uint32_t SOCKETS_GetHostByName( const char * pcHostName )
{
    uint32_t addr = 0;

    if( strlen( pcHostName ) <= ( size_t ) securesocketsMAX_DNS_NAME_LENGTH )
    {
        WIFI_GetHostIP( ( char * ) pcHostName, ( uint8_t * ) &addr );
    }
    else
    {
        addr = 0;
        configPRINTF( ( "Host name (%s) too long!", pcHostName ) );
    }

    return addr;
}

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

BaseType_t SOCKETS_Init( void )
{
    BaseType_t xResult = pdPASS;

    return xResult;
}

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