/******************************************************************************
 * Copyright (c) Huawei Technologies Co., Ltd. 2018-2019. All rights reserved.
 * iSulad licensed under the Mulan PSL v1.
 * You can use this software according to the terms and conditions of the Mulan PSL v1.
 * You may obtain a copy of Mulan PSL v1 at:
 *     http://license.coscl.org.cn/MulanPSL
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR
 * PURPOSE.
 * See the Mulan PSL v1 for more details.
 * Author: tanyifeng
 * Create: 2018-11-08
 * Description: provide container http function
 ******************************************************************************/
#include <curl/curl.h>
#include <curl/easy.h>
#include <string.h>

#include "http.h"
#include "buffer.h"
#include "log.h"
#include "utils.h"

size_t fwrite_buffer(const char *ptr, size_t eltsize, size_t nmemb, void *buffer_)
{
    size_t size = eltsize * nmemb;
    struct Buffer *buffer = buffer_;
    int status = 0;

    status = buffer_append(buffer, ptr, size);
    if (status != 0) {
        ERROR("Failed to write buffer\n");
    }
    return size;
}

size_t fwrite_file(const void *ptr, size_t size, size_t nmemb, void *stream)
{
    size_t written = fwrite(ptr, size, nmemb, (FILE *)stream);
    return written;
}

size_t fwrite_null(char *ptr, size_t eltsize, size_t nmemb, void *strbuf)
{
    return eltsize * nmemb;
}

void free_http_get_options(struct http_get_options *options)
{
    if (options == NULL) {
        return;
    }
    free(options->accepts);
    options->accepts = NULL;

    free(options->authorization);
    options->authorization = NULL;

    free(options->unix_socket_path);
    options->unix_socket_path = NULL;

    /* The options->output is a FILE pointer, we should not free it here */
    free(options);
    return;
}

void http_global_init(void)
{
    curl_global_init(CURL_GLOBAL_NOTHING);
}

void http_global_cleanup(void)
{
    curl_global_cleanup();
}

struct curl_slist *http_get_chunk_header(const struct http_get_options *options)
{
    int ret = 0;
    int nret;
    size_t len = 0;
    struct curl_slist *chunk = NULL;
    char *header = NULL;

    if (options->with_header_auth && options->authorization) {
        if (strlen(options->authorization) > (SIZE_MAX - strlen("Authorization: ")) - 1) {
            ERROR("Invalid authorization option");
            ret = -1;
            goto out;
        }
        len = strlen(options->authorization) + strlen("Authorization: ") + 1;
        header = util_common_calloc_s(len);
        if (header == NULL) {
            ERROR("Out of memory");
            ret = -1;
            goto out;
        }
        nret = snprintf(header, len, "Authorization: %s", options->authorization);
        if (nret < 0 || (size_t)nret >= len) {
            ERROR("Failed to print string");
            ret = -1;
            goto out;
        }
        chunk = curl_slist_append(chunk, header);
        free(header);
        header = NULL;
    }

    if (options->with_header_json) {
        chunk = curl_slist_append(chunk, "Content-Type: application/json");
        // Disable "Expect: 100-continue"
        chunk = curl_slist_append(chunk, "Expect:");
    }

    if (options->with_header_accept && options->accepts) {
        if (strlen(options->accepts) > (SIZE_MAX - strlen("Accept: ")) - 1) {
            ERROR("Invalid accepts option");
            ret = -1;
            goto out;
        }
        len = strlen(options->accepts) + strlen("Accept: ") + 1;
        header = util_common_calloc_s(len);
        if (header == NULL) {
            ERROR("Out of memory");
            ret = -1;
            goto out;
        }
        nret = snprintf(header, len, "Accept: %s", options->accepts);
        if (nret < 0 || (size_t)nret >= len) {
            ERROR("Failed to print string");
            ret = -1;
            goto out;
        }
        chunk = curl_slist_append(chunk, header);
        free(header);
        header = NULL;
    }
out:
    if (ret) {
        curl_slist_free_all(chunk);
        chunk = NULL;
    }
    free(header);

    return chunk;
}

static int http_custom_options(CURL *curl_handle, const struct http_get_options *options)
{
    int ret = 0;

    if (curl_handle == NULL || options == NULL) {
        return -1;
    }

    if (options->unix_socket_path) {
        curl_easy_setopt(curl_handle, CURLOPT_UNIX_SOCKET_PATH, options->unix_socket_path);
    }

    if (options->with_head) {
        curl_easy_setopt(curl_handle, CURLOPT_HEADER, 1L);
    }

    if (options->with_body == 0) {
        curl_easy_setopt(curl_handle, CURLOPT_NOBODY, 1L);
    }

    /* disable progress meter, set to 0L to enable and disable debug output */
    if (options->show_progress == 0) {
        curl_easy_setopt(curl_handle, CURLOPT_NOPROGRESS, 1L);
    } else if (options->show_progress && options->progressinfo && options->progress_info_op) {
        curl_easy_setopt(curl_handle, CURLOPT_PROGRESSFUNCTION, options->progress_info_op);
        /* pass the struct pointer into the progress function */
        curl_easy_setopt(curl_handle, CURLOPT_PROGRESSDATA, options->progressinfo);
        curl_easy_setopt(curl_handle, CURLOPT_NOPROGRESS, 0L);
    } else {
        curl_easy_setopt(curl_handle, CURLOPT_NOPROGRESS, 0L);
    }

    if (options->input) {
        curl_easy_setopt(curl_handle, CURLOPT_POSTFIELDS, options->input);
        curl_easy_setopt(curl_handle, CURLOPT_POSTFIELDSIZE, options->input_len);
        curl_easy_setopt(curl_handle, CURLOPT_POST, 1L);
    }

    return ret;
}

static void close_file(FILE *pagefile)
{
    if (pagefile != NULL) {
        fclose(pagefile);
    }
}

static void free_rpath(char *rpath)
{
    free(rpath);
}

static void check_buf_len(const char *errbuf)
{
    size_t len = 0;
    len = strlen(errbuf);
    if (len > 0) {
        fprintf(stderr, "%s%s", errbuf, ((errbuf[len - 1] != '\n') ? "\n" : ""));
    }
}

static void buffer_empty_on_condition(struct http_get_options *options)
{
    if (options == NULL) {
        return;
    }
    if (options->output && options->outputtype == HTTP_REQUEST_STRBUF) {
        buffer_empty(options->output);
    }

    if (options->with_header_auth && options->authorization) {
        options->with_header_auth = 0;
    }
}

static void curl_getinfo_on_condition(long *response_code, CURL *curl_handle, char **tmp)
{
    if (response_code != NULL) {
        curl_easy_getinfo(curl_handle, CURLINFO_RESPONSE_CODE, response_code);
    }
    curl_easy_getinfo(curl_handle, CURLINFO_REDIRECT_URL, tmp);
}

static int ensure_path_file(char **rpath, const struct http_get_options *options, FILE **pagefile)
{
    if (util_ensure_path(rpath, options->output)) {
        return -1;
    }
    *pagefile = util_fopen(*rpath, "w+");
    if (*pagefile == NULL) {
        ERROR("Failed to open file %s\n", options->output);
        return -1;
    }
    return 0;
}

static struct curl_slist *set_custom_header(CURL *curl_handle, const struct http_get_options *options)
{
    struct curl_slist *chunk = NULL;
    chunk = http_get_chunk_header(options);
    if (chunk) {
        /* set our custom set of headers */
        curl_easy_setopt(curl_handle, CURLOPT_HTTPHEADER, chunk);
    }
    return chunk;
}

int http_request(const char *url, struct http_get_options *options, long *response_code, int recursive_len)
{
#define MAX_REDIRCT_NUMS 32
    CURL *curl_handle = NULL;
    CURLcode curl_result = CURLE_OK;
    struct curl_slist *chunk = NULL;
    FILE *pagefile = NULL;
    char *rpath = NULL;
    int ret = 0;
    char errbuf[CURL_ERROR_SIZE] = { 0 };
    bool strbuf_args;
    bool file_args;
    char *redir_url = NULL;
    char *tmp = NULL;

    if (recursive_len + 1 >= MAX_REDIRCT_NUMS) {
        ERROR("reach the max redirect num");
        return -1;
    }
    http_global_init();
    /* init the curl session */
    curl_handle = curl_easy_init();
    if (curl_handle == NULL) {
        return -1;
    }

    /* set URL to get here */
    curl_easy_setopt(curl_handle, CURLOPT_URL, url);
    curl_easy_setopt(curl_handle, CURLOPT_NOSIGNAL, 1L);
    /* complete connection within 5 seconds */
    curl_easy_setopt(curl_handle, CURLOPT_CONNECTTIMEOUT, 5L);
    /* provide a buffer to store errors in */
    curl_easy_setopt(curl_handle, CURLOPT_ERRORBUFFER, errbuf);
    curl_easy_setopt(curl_handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
    curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYPEER, 0);

    ret = http_custom_options(curl_handle, options);
    if (ret) {
        goto out;
    }
    chunk = set_custom_header(curl_handle, options);

    strbuf_args = options->output && options->outputtype == HTTP_REQUEST_STRBUF;
    file_args = options->output && options->outputtype == HTTP_REQUEST_FILE;
    if (strbuf_args) {
        curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, options->output);
        curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, fwrite_buffer);
    } else if (file_args) {
        /* open the file */
        if (ensure_path_file(&rpath, options, &pagefile) == -1) {
            goto out;
        }
        curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, pagefile);
        curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, fwrite_file);
    } else {
        /* do nothing */
    }

    /* get it! */
    curl_result = curl_easy_perform(curl_handle);

    if (curl_result != CURLE_OK) {
        check_buf_len(errbuf);
        ret = -1;
    } else {
        curl_getinfo_on_condition(response_code, curl_handle, &tmp);
        if (tmp) {
            redir_url = util_strdup_s(tmp);
        }
    }

out:
    close_file(pagefile);
    free_rpath(rpath);

    /* cleanup curl stuff */
    curl_easy_cleanup(curl_handle);
    curl_slist_free_all(chunk);

    if (redir_url) {
        buffer_empty_on_condition(options);

        if (http_request(redir_url, options, response_code, recursive_len + 1)) {
            ERROR("Failed to get http request\n");
            ret = -1;
        }
        free(redir_url);
    }

    return ret;
}

int authz_http_request(const char *username, const char *action, char **resp)
{
    char *request_body = NULL;
    char err_msg[AUTHZ_ERROR_MSG_SIZE] = { 0 };
    long response_code = 0;
    int ret = 0;
    int nret = 0;
    size_t length = 0;
    struct http_get_options *options = NULL;
    if (strlen(username) > ((SIZE_MAX - strlen(action)) - strlen(":")) - 1) {
        ERROR("Invalid arguments");
        return -1;
    }
    length = strlen(username) + strlen(":") + strlen(action) + 1;
    request_body = util_common_calloc_s(length);
    if (request_body == NULL) {
        ERROR("Out of memory");
        *resp = util_strdup_s("Inernal server error: Out of memory");
        return -1;
    }
    nret = snprintf(request_body, length, "%s:%s", username, action);
    if (nret < 0 || (size_t)nret >= length) {
        ERROR("Failed to print string");
        free(request_body);
        return -1;
    }
    options = util_common_calloc_s(sizeof(struct http_get_options));
    if (options == NULL) {
        ERROR("Failed to malloc http_get_options");
        *resp = util_strdup_s("Inernal server error: Out of memory");
        free(request_body);
        return -1;
    }

    options->with_head = 1;
    options->with_header_json = 1;
    options->input = request_body;
    options->input_len = strlen(request_body);
    options->unix_socket_path = util_strdup_s(AUTHZ_UNIX_SOCK);

    ret = http_request(AUTHZ_REQUEST_URL, options, &response_code, 0);
    if (ret != 0) {
        ERROR("Failed to request authz plugin. Is server running ?");
        *resp = util_strdup_s("Failed to request authz plugin. Is server running ?");
        ret = -1;
        goto out;
    }
    if (response_code != StatusOK) {
        ret = -1;
        nret = snprintf(err_msg, sizeof(err_msg), "action '%s' for user '%s': permission denied", action, username);
        if (nret < 0 || (size_t)nret >= sizeof(err_msg)) {
            ERROR("Out of memory");
            *resp = util_strdup_s("Inernal server error: Out of memory");
            goto out;
        }
        *resp = util_strdup_s(err_msg);
        goto out;
    }

out:
    free(request_body);
    if (options != NULL) {
        free(options->unix_socket_path);
        free(options);
    }
    return ret;
}

