// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

#include "azure_c_shared_utility/httpapi.h"
#include "azure_c_shared_utility/httpheaders.h"
#include "azure_c_shared_utility/crt_abstractions.h"
#include "azure_c_shared_utility/xlogging.h"
#include "winsock2.h"
#include "sslsock.h"
#include "schnlsp.h"


#define MAX_HOSTNAME     64
#define TEMPORARY_BUFFER_SIZE 4096

#define CHAR_COUNT(A)   (sizeof(A) - 1)

DEFINE_ENUM_STRINGS(HTTPAPI_RESULT, HTTPAPI_RESULT_VALUES)

// data structure that contains all the information required to manage a connection
class HTTP_HANDLE_DATA
{
public:
    char   host[MAX_HOSTNAME];
    SOCKET _sock_fd;
    struct sockaddr_in _remoteHost;
};

// If Root CA is not found in CA database, dwFlags will be set to SSL_CERT_FLAG_ISSUER_UNKNOWN (value 1). In that case return SSL_ERR_CERT_UNKNOWN (value 10)
int CALLBACK SSLValidateCertHook(DWORD  dwType, LPVOID pvArg,
    DWORD  dwChainLen, LPBLOB pCertChain, DWORD dwFlags)
{
    DWORD dwRetValue = SSL_ERR_OKAY; // SSL_ERR_OKAY : The certificate is acceptable
    if (dwFlags)
        dwRetValue = SSL_ERR_CERT_UNKNOWN; // Issuer is unknown

    return dwRetValue;

}

// Initializes SSL layer, called only once on initialization
int SSLInit(SOCKET s)
{
    DWORD optval = SO_SEC_SSL;
    SSLVALIDATECERTHOOK hook;
    SSLPROTOCOLS protocolsToUse;
    int ret;


    if (setsockopt(s, SOL_SOCKET, SO_SECURE, (LPSTR)&optval, sizeof(optval)) ==
        SOCKET_ERROR)
    {
        goto SSLInit_Error;
    }

    hook.HookFunc = SSLValidateCertHook;
    hook.pvArg = NULL; //WSAIoctl cannot handle the pointer copy, use a global variable

    if (WSAIoctl(s, SO_SSL_SET_VALIDATE_CERT_HOOK,
        &hook, sizeof(SSLVALIDATECERTHOOK),
        NULL, 0, NULL, NULL, NULL) == SOCKET_ERROR)
    {
        goto SSLInit_Error;
    }

    //specify TLS1 protocol

    protocolsToUse.dwCount = 1;
    protocolsToUse.ProtocolList[0].dwFlags = 0;
    protocolsToUse.ProtocolList[0].dwProtocol = SSL_PROTOCOL_TLS1;
    protocolsToUse.ProtocolList[0].dwVersion = 0;

    if (WSAIoctl(s, SO_SSL_SET_PROTOCOLS,
        (LPVOID)&protocolsToUse, sizeof(protocolsToUse),
        NULL, 0, NULL, NULL, NULL) == SOCKET_ERROR)
    {
        goto SSLInit_Error;
    }

    return 0;

SSLInit_Error:

    ret = WSAGetLastError();
    return ret;
}

HTTPAPI_RESULT HTTPAPI_Init(void)
{
    WORD wVersionRequested;
    WSADATA wsaData;
    int err;

    LogInfo("HTTPAPI_Init::Start");
    time_t ctTime;
    ctTime = time(NULL);

    LogInfo("HTTAPI_Init::Time is now (UTC) %s", ctime(&ctTime));

    wVersionRequested = MAKEWORD(2, 2);

    err = WSAStartup(wVersionRequested, &wsaData);
    if (err != 0) {
        return HTTPAPI_INIT_FAILED;
    }

    if (LOBYTE(wsaData.wVersion) != 2 ||
        HIBYTE(wsaData.wVersion) != 2) {
        WSACleanup();
        return HTTPAPI_INIT_FAILED;
    }


    LogInfo("HTTPAPI_Init::End");
    return HTTPAPI_OK;
}

void HTTPAPI_Deinit(void)
{
    WSACleanup();
}

HTTP_HANDLE HTTPAPI_CreateConnection(const char* hostName)
{
    LogInfo("HTTPAPI_CreateConnection::Start");
    HTTP_HANDLE_DATA* handle = NULL;

    if (hostName)
    {
        LogInfo("HTTPAPI_CreateConnection::Connecting to %s", hostName);
        handle = new HTTP_HANDLE_DATA();
        if (strcpy_s(handle->host, MAX_HOSTNAME, hostName) != 0)
        {
            LogError("HTTPAPI_CreateConnection::Could not strcpy_s");
            delete handle;
            handle = NULL;
        }
        else
        {
            handle->_sock_fd = -1;
        }
    }
    else
    {
        LogInfo("HTTPAPI_CreateConnection:: null hostName parameter");
    }
    LogInfo("HTTPAPI_CreateConnection::End");

    return (HTTP_HANDLE)handle;
}

void HTTPAPI_CloseConnection(HTTP_HANDLE handle)
{
    HTTP_HANDLE_DATA* h = (HTTP_HANDLE_DATA*)handle;

    if (h)
    {
        LogInfo("HTTPAPI_CloseConnection to %s", h->host);
        if (h->_sock_fd != -1)
        {
            LogInfo("HTTPAPI_CloseConnection to %s", h->host);
            closesocket(h->_sock_fd);
            h->_sock_fd = -1;
        }
        LogInfo("HTTPAPI_CloseConnection (delete h) to %s", h->host);
        delete h;
    }
}

static int readLine(SOCKET _sock_fd, char* buf, const size_t size)
{
    // reads until \r\n is encountered. writes in buf all the characters
    // read until \r\n and returns the number of characters in the buffer.
    char* p = buf;
    char  c;
    if (recv(_sock_fd, &c, 1, 0) < 0)
        return -1;
    while (c != '\r') {
        if ((p - buf + 1) >= (int)size)
            return -1;
        *p++ = c;
        if (recv(_sock_fd, &c, 1, 0) < 0)
            return -1;
    }
    *p = 0;
    if (recv(_sock_fd, &c, 1, 0) < 0 || c != '\n') // skip \n
        return -1;
    return p - buf;
}

static int readChunk(SOCKET _sock_fd, char* buf, size_t size)
{
    size_t cur, offset;

    // read content with specified length, even if it is received
    // only in chunks due to fragmentation in the networking layer.
    // returns -1 in case of error.
    offset = 0;
    while (size > 0)
    {
        cur = recv(_sock_fd, buf + offset, size, 0);

        // end of stream reached
        if (cur == 0)
            return offset;

        // read cur bytes (might be less than requested)
        size -= cur;
        offset += cur;
    }

    return offset;
}

static int skipN(SOCKET _sock_fd, size_t n, char* buf, size_t size)
{
    size_t org = n;
    // read and abandon response content with specified length
    // returns -1 in case of error.
    while (n > size)
    {
        if (readChunk(_sock_fd, (char*)buf, size) < 0)
            return -1;

        n -= size;
    }

    if (readChunk(_sock_fd, (char*)buf, n) < 0)
        return -1;

    return org;
}

//Note: This function assumes that "Host:" and "Content-Length:" headers are setup
//      by the caller of HTTPAPI_ExecuteRequest() (which is true for httptransport.c).
HTTPAPI_RESULT HTTPAPI_ExecuteRequest(HTTP_HANDLE handle, HTTPAPI_REQUEST_TYPE requestType, const char* relativePath,
    HTTP_HEADERS_HANDLE httpHeadersHandle, const unsigned char* content,
    size_t contentLength, unsigned int* statusCode,
    HTTP_HEADERS_HANDLE responseHeadersHandle, BUFFER_HANDLE responseContent)
{
    LogInfo("HTTPAPI_ExecuteRequest::Start");

    HTTPAPI_RESULT result;
    size_t  headersCount;
    char    buf[TEMPORARY_BUFFER_SIZE];
    int     ret;
    size_t  bodyLength = 0;
    bool    chunked = false;
    const unsigned char* receivedContent;

    const char* method = (requestType == HTTPAPI_REQUEST_GET) ? "GET"
        : (requestType == HTTPAPI_REQUEST_POST) ? "POST"
        : (requestType == HTTPAPI_REQUEST_PUT) ? "PUT"
        : (requestType == HTTPAPI_REQUEST_DELETE) ? "DELETE"
        : (requestType == HTTPAPI_REQUEST_PATCH) ? "PATCH"
        : NULL;

    if (handle == NULL ||
        relativePath == NULL ||
        httpHeadersHandle == NULL ||
        method == NULL ||
        HTTPHeaders_GetHeaderCount(httpHeadersHandle, &headersCount) != HTTP_HEADERS_OK)
    {
        result = HTTPAPI_INVALID_ARG;
        LogError("(result = %s)", ENUM_TO_STRING(HTTPAPI_RESULT, result));
        goto exit;
    }

    HTTP_HANDLE_DATA* httpHandle = (HTTP_HANDLE_DATA*)handle;

    if (httpHandle->_sock_fd == -1)
    {
        // Make the connection
        if ((httpHandle->_sock_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        {
            LogError("init_socket failed");
            result = HTTPAPI_INIT_FAILED;
            goto exit;
        }
        else
        {
            // IP Address
            char address[5];
            char *p_address = address;

            memset(&httpHandle->_remoteHost, 0, sizeof(struct sockaddr_in));

            // Dot-decimal notation
            int result = sscanf(httpHandle->host, "%3u.%3u.%3u.%3u",
                (unsigned int*)&address[0], (unsigned int*)&address[1],
                (unsigned int*)&address[2], (unsigned int*)&address[3]);

            if (result != 4) {
                // Resolve address with DNS
                struct hostent *host_address = gethostbyname(httpHandle->host);
                if (host_address == NULL)
                {
                    LogError("set_address failed");
                    result = HTTPAPI_INVALID_ARG;
                    goto exit;
                }
                p_address = (char*)host_address->h_addr_list[0];
            }

            memcpy((char*)&httpHandle->_remoteHost.sin_addr.s_addr, p_address, 4);

            // Address family
            httpHandle->_remoteHost.sin_family = AF_INET;

            // Set port
            httpHandle->_remoteHost.sin_port = htons(443);

            if (SSLInit(httpHandle->_sock_fd))
            {
                LogError("SSLInit failed");
                result = HTTPAPI_ERROR;
                goto exit;
            }

            if (connect(httpHandle->_sock_fd, (const struct sockaddr *) &httpHandle->_remoteHost, sizeof(httpHandle->_remoteHost)) < 0)
            {
                closesocket(httpHandle->_sock_fd);
                httpHandle->_sock_fd = -1;
                LogError("connect failed");
                result = HTTPAPI_ERROR;
                goto exit;
            }
        }
    }

    //Send request
    if ((ret = _snprintf(buf, sizeof(buf), "%s %s HTTP/1.1\r\n", method, relativePath)) < 0
        || ret >= sizeof(buf))
    {
        result = HTTPAPI_STRING_PROCESSING_ERROR;
        LogError("(result = %s)", ENUM_TO_STRING(HTTPAPI_RESULT, result));
        goto exit;
    }
    LogInfo("HTTPAPI_ExecuteRequest::Sending=%*.*s", strlen(buf), strlen(buf), buf);
    if (send(httpHandle->_sock_fd, buf, strlen(buf), 0) < 0)
    {
        result = HTTPAPI_SEND_REQUEST_FAILED;
        LogError("(result = %s)", ENUM_TO_STRING(HTTPAPI_RESULT, result));
        goto exit;
    }

    //Send default headers
    for (size_t i = 0; i < headersCount; i++)
    {
        char* header;
        if (HTTPHeaders_GetHeader(httpHeadersHandle, i, &header) != HTTP_HEADERS_OK)
        {
            result = HTTPAPI_HTTP_HEADERS_FAILED;
            LogError("(result = %s)", ENUM_TO_STRING(HTTPAPI_RESULT, result));
            goto exit;
        }
        LogInfo("HTTPAPI_ExecuteRequest::Sending=%*.*s", strlen(header), strlen(header), header);
        if (send(httpHandle->_sock_fd, header, strlen(header), 0) < 0)
        {
            result = HTTPAPI_SEND_REQUEST_FAILED;
            LogError("(result = %s)", ENUM_TO_STRING(HTTPAPI_RESULT, result));
            free(header);
            goto exit;
        }
        if (send(httpHandle->_sock_fd, "\r\n", 2, 0) < 0)
        {
            result = HTTPAPI_SEND_REQUEST_FAILED;
            LogError("(result = %s)", ENUM_TO_STRING(HTTPAPI_RESULT, result));
            free(header);
            goto exit;
        }
        free(header);
    }

    //Close headers
    if (send(httpHandle->_sock_fd, "\r\n", 2, 0) < 0)
    {
        result = HTTPAPI_SEND_REQUEST_FAILED;
        LogError("(result = %s)", ENUM_TO_STRING(HTTPAPI_RESULT, result));
        goto exit;
    }

    //Send data (if available)
    if (content && contentLength > 0)
    {
        LogInfo("HTTPAPI_ExecuteRequest::Sending data=%*.*s", contentLength, contentLength, content);
        if (send(httpHandle->_sock_fd, (char*)content, contentLength, 0) < 0)
        {
            result = HTTPAPI_SEND_REQUEST_FAILED;
            LogError("(result = %s)", ENUM_TO_STRING(HTTPAPI_RESULT, result));
            goto exit;
        }
    }

    //Receive response
    if (readLine(httpHandle->_sock_fd, buf, sizeof(buf)) < 0)
    {
        result = HTTPAPI_READ_DATA_FAILED;
        LogError("(result = %s)", ENUM_TO_STRING(HTTPAPI_RESULT, result));
        goto exit;
    }

    //Parse HTTP response
    if (sscanf(buf, "HTTP/%*d.%*d %d %*[^\r\n]", &ret) != 1)
    {
        //Cannot match string, error
        LogInfo("HTTPAPI_ExecuteRequest::Not a correct HTTP answer=%s", buf);
        result = HTTPAPI_READ_DATA_FAILED;
        LogError("(result = %s)", ENUM_TO_STRING(HTTPAPI_RESULT, result));
        goto exit;
    }
    if (statusCode)
        *statusCode = ret;
    LogInfo("HTTPAPI_ExecuteRequest::Received response=%*.*s", strlen(buf), strlen(buf), buf);

    //Read HTTP response headers
    if (readLine(httpHandle->_sock_fd, buf, sizeof(buf)) < 0)
    {
        result = HTTPAPI_READ_DATA_FAILED;
        LogError("(result = %s)", ENUM_TO_STRING(HTTPAPI_RESULT, result));
        goto exit;
    }

    while (buf[0])
    {
        const char ContentLength[] = "content-length:";
        const char TransferEncoding[] = "transfer-encoding:";

        LogInfo("Receiving header=%*.*s", strlen(buf), strlen(buf), buf);

        if (_strnicmp(buf, ContentLength, CHAR_COUNT(ContentLength)) == 0)
        {
            if (sscanf(buf + CHAR_COUNT(ContentLength), " %d", &bodyLength) != 1)
            {
                result = HTTPAPI_READ_DATA_FAILED;
                LogError("(result = %s)", ENUM_TO_STRING(HTTPAPI_RESULT, result));
                goto exit;
            }
        }
        else if (_strnicmp(buf, TransferEncoding, CHAR_COUNT(TransferEncoding)) == 0)
        {
            const char* p = buf + CHAR_COUNT(TransferEncoding);
            while (isspace(*p)) p++;
            if (_stricmp(p, "chunked") == 0)
                chunked = true;
        }

        char* whereIsColon = strchr((char*)buf, ':');
        if (whereIsColon && responseHeadersHandle != NULL)
        {
            *whereIsColon = '\0';
            HTTPHeaders_AddHeaderNameValuePair(responseHeadersHandle, buf, whereIsColon + 1);
        }

        if (readLine(httpHandle->_sock_fd, buf, sizeof(buf)) < 0)
        {
            result = HTTPAPI_READ_DATA_FAILED;
            LogError("(result = %s)", ENUM_TO_STRING(HTTPAPI_RESULT, result));
            goto exit;
        }
    }

    //Read HTTP response body
    LogInfo("HTTPAPI_ExecuteRequest::Receiving body=%d,%x", bodyLength, responseContent);
    if (!chunked)
    {
        if (bodyLength)
        {
            if (responseContent != NULL)
            {
                if (BUFFER_pre_build(responseContent, bodyLength) != 0)
                {
                    result = HTTPAPI_ALLOC_FAILED;
                    LogError("(result = %s)", ENUM_TO_STRING(HTTPAPI_RESULT, result));
                }
                else if (BUFFER_content(responseContent, &receivedContent) != 0)
                {
                    (void)BUFFER_unbuild(responseContent);

                    result = HTTPAPI_ALLOC_FAILED;
                    LogError("(result = %s)", ENUM_TO_STRING(HTTPAPI_RESULT, result));
                }

                if (readChunk(httpHandle->_sock_fd, (char*)receivedContent, bodyLength) < 0)
                {
                    result = HTTPAPI_READ_DATA_FAILED;
                    LogError("(result = %s)", ENUM_TO_STRING(HTTPAPI_RESULT, result));
                    goto exit;
                }
                else
                {
                    LogInfo("HTTPAPI_ExecuteRequest::Received response body=%*.*s", bodyLength, bodyLength, receivedContent);
                    result = HTTPAPI_OK;
                }
            }
            else
            {
                (void)skipN(httpHandle->_sock_fd, bodyLength, buf, sizeof(buf));
                result = HTTPAPI_OK;
            }
        }
        else
        {
            result = HTTPAPI_OK;
        }
    }
    else
    {
        size_t size = 0;
        result = HTTPAPI_OK;
        for (;;)
        {
            int chunkSize;
            if (readLine(httpHandle->_sock_fd, buf, sizeof(buf)) < 0)    // read [length in hex]/r/n
            {
                result = HTTPAPI_READ_DATA_FAILED;
                LogError("(result = %s)", ENUM_TO_STRING(HTTPAPI_RESULT, result));
                goto exit;
            }
            if (sscanf(buf, "%x", &chunkSize) != 1)     // chunkSize is length of next line (/r/n is not counted)
            {
                //Cannot match string, error
                result = HTTPAPI_RECEIVE_RESPONSE_FAILED;
                LogError("(result = %s)", ENUM_TO_STRING(HTTPAPI_RESULT, result));
                goto exit;
            }

            if (chunkSize == 0)
            {
                // 0 length means next line is just '\r\n' and end of chunks
                if (readChunk(httpHandle->_sock_fd, (char*)buf, 2) < 0
                    || buf[0] != '\r' || buf[1] != '\n') // skip /r/n
                {
                    (void)BUFFER_unbuild(responseContent);

                    result = HTTPAPI_READ_DATA_FAILED;
                    LogError("(result = %s)", ENUM_TO_STRING(HTTPAPI_RESULT, result));
                    goto exit;
                }
                break;
            }
            else
            {
                if (responseContent != NULL)
                {
                    if (BUFFER_enlarge(responseContent, chunkSize) != 0)
                    {
                        (void)BUFFER_unbuild(responseContent);

                        result = HTTPAPI_ALLOC_FAILED;
                        LogError("(result = %s)", ENUM_TO_STRING(HTTPAPI_RESULT, result));
                    }
                    else if (BUFFER_content(responseContent, &receivedContent) != 0)
                    {
                        (void)BUFFER_unbuild(responseContent);

                        result = HTTPAPI_ALLOC_FAILED;
                        LogError("(result = %s)", ENUM_TO_STRING(HTTPAPI_RESULT, result));
                    }

                    if (readChunk(httpHandle->_sock_fd, (char*)receivedContent + size, chunkSize) < 0)
                    {
                        result = HTTPAPI_READ_DATA_FAILED;
                        LogError("(result = %s)", ENUM_TO_STRING(HTTPAPI_RESULT, result));
                        goto exit;
                    }
                }
                else
                {
                    if (skipN(httpHandle->_sock_fd, chunkSize, buf, sizeof(buf)) < 0)
                    {
                        result = HTTPAPI_READ_DATA_FAILED;
                        LogError("(result = %s)", ENUM_TO_STRING(HTTPAPI_RESULT, result));
                        goto exit;
                    }
                }

                if (readChunk(httpHandle->_sock_fd, (char*)buf, 2) < 0
                    || buf[0] != '\r' || buf[1] != '\n') // skip /r/n
                {
                    result = HTTPAPI_READ_DATA_FAILED;
                    LogError("(result = %s)", ENUM_TO_STRING(HTTPAPI_RESULT, result));
                    goto exit;
                }
                size += chunkSize;
            }
        }

        if (size > 0)
        {
            LogInfo("HTTPAPI_ExecuteRequest::Received chunk body=%*.*s", (int)size, (int)size, (const char*)responseContent);
        }
    }

exit:
    LogInfo("HTTPAPI_ExecuteRequest::End=%d", result);
    return result;
}

HTTPAPI_RESULT HTTPAPI_SetOption(HTTP_HANDLE handle, const char* optionName, const void* value)
{
    HTTPAPI_RESULT result;
    if (
        (handle == NULL) ||
        (optionName == NULL) ||
        (value == NULL)
        )
    {
        result = HTTPAPI_INVALID_ARG;
        LogError("invalid parameter (NULL) passed to HTTPAPI_SetOption");
    }
    else
    {
        result = HTTPAPI_INVALID_ARG;
        LogError("unknown option %s", optionName);
    }
    return result;
}

HTTPAPI_RESULT HTTPAPI_CloneOption(const char* optionName, const void* value, const void** savedValue)
{
    HTTPAPI_RESULT result;
    if (
        (optionName == NULL) ||
        (value == NULL) ||
        (savedValue == NULL)
        )
    {
        result = HTTPAPI_INVALID_ARG;
        LogError("invalid argument(NULL) passed to HTTPAPI_CloneOption");
    }
    else
    {
        *savedValue = NULL;
        result = HTTPAPI_INVALID_ARG;
        LogError("unknown option %s", optionName);
    }
    return result;
}
