#include "oss_auth.h"
#include "aos_log.h"
#include "oss_util.h"

static const char *g_s_oss_sub_resource_list[] = {
    "acl",
    "uploadId",
    "uploads",
    "partNumber",
    "response-content-type",
    "response-content-language",
    "response-expires",
    "response-cache-control",
    "response-content-disposition",
    "response-content-encoding",
    "append",
    "position",
    "lifecycle",
    "delete",
    "live",
    "status",
    "comp",
    "vod",
    "startTime",
    "endTime",
    "x-oss-process",
    "security-token",
    NULL,
};

static int is_oss_sub_resource(const char *str);
static int is_oss_canonicalized_header(const char *str);
static int oss_get_canonicalized_headers(aos_pool_t *p, 
        const aos_table_t *headers, aos_buf_t *signbuf);
static int oss_get_canonicalized_resource(aos_pool_t *p, 
        const aos_table_t *params, aos_buf_t *signbuf);
static int oss_get_canonicalized_params(aos_pool_t *p,
    const aos_table_t *params, aos_buf_t *signbuf);

static int is_oss_sub_resource(const char *str)
{
    int i = 0;
    for ( ; g_s_oss_sub_resource_list[i]; i++) {
        if (apr_strnatcmp(g_s_oss_sub_resource_list[i], str) == 0) {
            return 1;
        }
    }
    return 0;
}

static int is_oss_canonicalized_header(const char *str)
{
    size_t len = strlen(OSS_CANNONICALIZED_HEADER_PREFIX);
    return strncasecmp(str, OSS_CANNONICALIZED_HEADER_PREFIX, len) == 0;
}

static int oss_get_canonicalized_headers(aos_pool_t *p, 
                                         const aos_table_t *headers, 
                                         aos_buf_t *signbuf)
{
    int pos;
    int meta_count = 0;
    int i;
    int len;
    const aos_array_header_t *tarr;
    const aos_table_entry_t *telts;
    char **meta_headers;
    const char *value;
    aos_string_t tmp_str;
    char *tmpbuf = (char*)malloc(AOS_MAX_HEADER_LEN + 1);
    if (NULL == tmpbuf) {
        aos_error_log("malloc %d memory failed.", AOS_MAX_HEADER_LEN + 1);
        return AOSE_OVER_MEMORY;
    }

    if (apr_is_empty_table(headers)) {
        free(tmpbuf);
        return AOSE_OK;
    }

    // sort user meta header
    tarr = aos_table_elts(headers);
    telts = (aos_table_entry_t*)tarr->elts;
    meta_headers = aos_pcalloc(p, tarr->nelts * sizeof(char*));
    for (pos = 0; pos < tarr->nelts; ++pos) {
        if (is_oss_canonicalized_header(telts[pos].key)) {
            aos_string_t key = aos_string(telts[pos].key);
            aos_string_tolower(&key);
            meta_headers[meta_count++] = key.data;
        }
    }
    if (meta_count == 0) {
        free(tmpbuf);
        return AOSE_OK;
    }
    aos_gnome_sort((const char **)meta_headers, meta_count);

    // sign string
    for (i = 0; i < meta_count; ++i) {
        value = apr_table_get(headers, meta_headers[i]);
        aos_str_set(&tmp_str, value);
        aos_strip_space(&tmp_str);
        len = apr_snprintf(tmpbuf, AOS_MAX_HEADER_LEN + 1, "%s:%.*s", 
                           meta_headers[i], tmp_str.len, tmp_str.data);
        if (len > AOS_MAX_HEADER_LEN) {
            free(tmpbuf);
            aos_error_log("user meta header too many, %d > %d.", 
                          len, AOS_MAX_HEADER_LEN);
            return AOSE_INVALID_ARGUMENT;
        }
        tmp_str.data = tmpbuf;
        tmp_str.len = len;
        aos_buf_append_string(p, signbuf, tmpbuf, len);
        aos_buf_append_string(p, signbuf, "\n", sizeof("\n")-1);
    }

    free(tmpbuf);
    return AOSE_OK;
}

static int oss_get_canonicalized_resource(aos_pool_t *p, 
                                          const aos_table_t *params, 
                                          aos_buf_t *signbuf)
{
    int pos;
    int subres_count = 0;
    int i;
    int len;
    char sep;
    const char *value;
    char tmpbuf[AOS_MAX_QUERY_ARG_LEN+1];
    char **subres_headers;
    const aos_array_header_t *tarr;
    const aos_table_entry_t *telts;

    if (apr_is_empty_table(params)) {
        return AOSE_OK;
    }

    // sort sub resource param
    tarr = aos_table_elts(params);
    telts = (aos_table_entry_t*)tarr->elts;
    subres_headers = aos_pcalloc(p, tarr->nelts * sizeof(char*));
    for (pos = 0; pos < tarr->nelts; ++pos) {
        if (is_oss_sub_resource(telts[pos].key)) {
            subres_headers[subres_count++] = telts[pos].key;
        }
    }
    if (subres_count == 0) {
        return AOSE_OK;
    }
    aos_gnome_sort((const char **)subres_headers, subres_count);

    // sign string
    sep = '?';
    for (i = 0; i < subres_count; ++i) {
        value = apr_table_get(params, subres_headers[i]);
        if (value != NULL && *value != '\0') {
            len = apr_snprintf(tmpbuf, sizeof(tmpbuf), "%c%s=%s", 
                    sep, subres_headers[i], value);
        } else {
            len = apr_snprintf(tmpbuf, sizeof(tmpbuf), "%c%s", 
                    sep, subres_headers[i]);
        }
        if (len >= AOS_MAX_QUERY_ARG_LEN) {
            aos_error_log("http query params too long, %s.", tmpbuf);
            return AOSE_INVALID_ARGUMENT;
        }
        aos_buf_append_string(p, signbuf, tmpbuf, len);
        sep = '&';
    }

    return AOSE_OK;
}    

int oss_get_string_to_sign(aos_pool_t *p, 
                           http_method_e method, 
                           const aos_string_t *canon_res,
                           const aos_table_t *headers, 
                           const aos_table_t *params, 
                           aos_string_t *signstr)
{
    int res;
    aos_buf_t *signbuf;
    const char *value;
    aos_str_null(signstr);

    signbuf = aos_create_buf(p, 1024);

#define signbuf_append_from_headers(KEY) do {                            \
        if ((value = apr_table_get(headers, KEY)) != NULL) {            \
            aos_buf_append_string(p, signbuf, value, strlen(value));    \
        }                                                               \
        aos_buf_append_string(p, signbuf, "\n", sizeof("\n")-1);        \
    } while (0)

#define signbuf_append(VALUE, LEN) do {                                 \
        aos_buf_append_string(p, signbuf, VALUE, LEN);                  \
        aos_buf_append_string(p, signbuf, "\n", sizeof("\n")-1);        \
    } while (0)
    
    value = aos_http_method_to_string(method);
    signbuf_append(value, strlen(value));    

    signbuf_append_from_headers(OSS_CONTENT_MD5);
    signbuf_append_from_headers(OSS_CONTENT_TYPE);

    // date
    if ((value = apr_table_get(headers, OSS_CANNONICALIZED_HEADER_DATE)) == NULL) {
        value = apr_table_get(headers, OSS_DATE);
    }
    if (NULL == value || *value == '\0') {
        aos_error_log("http header date is empty.");
        return AOSE_INVALID_ARGUMENT;
    }
    signbuf_append(value, strlen(value));

    // user meta headers
    if ((res = oss_get_canonicalized_headers(p, headers, signbuf)) != AOSE_OK) {
        return res;
    }

    // canonicalized resource
    aos_buf_append_string(p, signbuf, canon_res->data, canon_res->len);
    
    if (params != NULL && (res = oss_get_canonicalized_resource(p, params, signbuf)) != AOSE_OK) {
        return res;
    }

    // result
    signstr->data = (char *)signbuf->pos;
    signstr->len = aos_buf_size(signbuf);

    return AOSE_OK;
}

void oss_sign_headers(aos_pool_t *p, 
                      const aos_string_t *signstr, 
                      const aos_string_t *access_key_id,
                      const aos_string_t *access_key_secret, 
                      aos_table_t *headers)
{
    int b64Len;
    char *value;
    unsigned char hmac[20];
    char b64[((20 + 1) * 4) / 3];

    HMAC_SHA1(hmac, (unsigned char *)access_key_secret->data, access_key_secret->len,
              (unsigned char *)signstr->data, signstr->len);

    // Now base-64 encode the results
    b64Len = aos_base64_encode(hmac, 20, b64);
    value = apr_psprintf(p, "OSS %.*s:%.*s", access_key_id->len, access_key_id->data, b64Len, b64);
    apr_table_addn(headers, OSS_AUTHORIZATION, value);

    return;
}

int oss_get_signed_headers(aos_pool_t *p, 
                           const aos_string_t *access_key_id, 
                           const aos_string_t *access_key_secret,
                           const aos_string_t* canon_res, 
                           aos_http_request_t *req)
{
    int res;
    aos_string_t signstr;

    res = oss_get_string_to_sign(p, req->method, canon_res, 
                                 req->headers, req->query_params, &signstr);
    
    if (res != AOSE_OK) {
        return res;
    }
    
    aos_debug_log("signstr:%.*s.", signstr.len, signstr.data);

    oss_sign_headers(p, &signstr, access_key_id, access_key_secret, req->headers);

    return AOSE_OK;
}

int oss_sign_request(aos_http_request_t *req, 
                     const oss_config_t *config)
{
    aos_string_t canon_res;
    char canon_buf[AOS_MAX_URI_LEN];
    char datestr[AOS_MAX_GMT_TIME_LEN];
    const char *value;
    int res = AOSE_OK;
    int len = 0;
    
    len = strlen(req->resource);
    if (len >= AOS_MAX_URI_LEN - 1) {
        aos_error_log("http resource too long, %s.", req->resource);
        return AOSE_INVALID_ARGUMENT;
    }

    canon_res.data = canon_buf;
    canon_res.len = apr_snprintf(canon_buf, sizeof(canon_buf), "/%s", req->resource);

    if ((value = apr_table_get(req->headers, OSS_CANNONICALIZED_HEADER_DATE)) == NULL) {
        aos_get_gmt_str_time(datestr);
        apr_table_set(req->headers, OSS_DATE, datestr);
    }

    res = oss_get_signed_headers(req->pool, &config->access_key_id, 
                                 &config->access_key_secret, &canon_res, req);
    return res;
}

int get_oss_request_signature(const oss_request_options_t *options, 
                              aos_http_request_t *req,
                              const aos_string_t *expires, 
                              aos_string_t *signature)
{
    aos_string_t canon_res;
    char canon_buf[AOS_MAX_URI_LEN];
    const char *value;
    aos_string_t signstr;
    int res = AOSE_OK;
    int b64Len;
    unsigned char hmac[20];
    char b64[((20 + 1) * 4) / 3];

    canon_res.data = canon_buf;
    canon_res.len = apr_snprintf(canon_buf, sizeof(canon_buf), "/%s", req->resource);

    apr_table_set(req->headers, OSS_DATE, expires->data);

    if ((res = oss_get_string_to_sign(options->pool, req->method, &canon_res, 
        req->headers, req->query_params, &signstr))!= AOSE_OK) {
        return res;
    }

    HMAC_SHA1(hmac, (unsigned char *)options->config->access_key_secret.data, 
              options->config->access_key_secret.len,
              (unsigned char *)signstr.data, signstr.len);

    b64Len = aos_base64_encode(hmac, 20, b64);
    value = apr_psprintf(options->pool, "%.*s", b64Len, b64);
    aos_str_set(signature, value);

    return res;
}

int oss_get_signed_url(const oss_request_options_t *options, 
                       aos_http_request_t *req,
                       const aos_string_t *expires, 
                       aos_string_t *signed_url)
{
    char *signed_url_str;
    aos_string_t querystr;
    char uristr[3*AOS_MAX_URI_LEN+1];
    int res = AOSE_OK;
    aos_string_t signature;
    const char *proto;

    if (options->config->sts_token.data != NULL) {
        apr_table_set(req->query_params, OSS_SECURITY_TOKEN, options->config->sts_token.data);
    }

    res = get_oss_request_signature(options, req, expires, &signature);
    if (res != AOSE_OK) {
        return res;
    }

    apr_table_set(req->query_params, OSS_ACCESSKEYID, options->config->access_key_id.data);
    apr_table_set(req->query_params, OSS_EXPIRES, expires->data);
    apr_table_set(req->query_params, OSS_SIGNATURE, signature.data);

    uristr[0] = '\0';
    aos_str_null(&querystr);
    res = aos_url_encode(uristr, req->uri, AOS_MAX_URI_LEN);
    if (res != AOSE_OK) {
        return res;
    }

    res = aos_query_params_to_string(options->pool, req->query_params, &querystr);
    if (res != AOSE_OK) {
        return res;
    }

    proto = strlen(req->proto) != 0 ? req->proto : AOS_HTTP_PREFIX;
    signed_url_str = apr_psprintf(options->pool, "%s%s/%s%.*s",
                                  proto, req->host, uristr,
                                  querystr.len, querystr.data);
    aos_str_set(signed_url, signed_url_str);

    return res;
}

int oss_get_rtmp_signed_url(const oss_request_options_t *options,
                            aos_http_request_t *req,
                            const aos_string_t *expires,
                            const aos_string_t *play_list_name,
                            aos_table_t *params,
                            aos_string_t *signed_url)
{
    char *signed_url_str;
    aos_string_t querystr;
    char uristr[3*AOS_MAX_URI_LEN+1];
    int res = AOSE_OK;
    aos_string_t signature;
    int pos = 0;
    const aos_array_header_t *tarr;
    const aos_table_entry_t *telts;

    if (NULL != params) {
        tarr = aos_table_elts(params);
        telts = (aos_table_entry_t*)tarr->elts;
        for (pos = 0; pos < tarr->nelts; ++pos) {
            apr_table_set(req->query_params, telts[pos].key, telts[pos].val);
        }
    }
    apr_table_set(req->query_params, OSS_PLAY_LIST_NAME, play_list_name->data);

    res = get_oss_rtmp_request_signature(options, req, expires,&signature);
    if (res != AOSE_OK) {
        return res;
    }

    apr_table_set(req->query_params, OSS_ACCESSKEYID,
                  options->config->access_key_id.data);
    apr_table_set(req->query_params, OSS_EXPIRES, expires->data);
    apr_table_set(req->query_params, OSS_SIGNATURE, signature.data);

    uristr[0] = '\0';
    aos_str_null(&querystr);
    res = aos_url_encode(uristr, req->uri, AOS_MAX_URI_LEN);
    if (res != AOSE_OK) {
        return res;
    }

    res = aos_query_params_to_string(options->pool, req->query_params, &querystr);
    if (res != AOSE_OK) {
        return res;
    }

    signed_url_str = apr_psprintf(options->pool, "%s%s/%s%.*s",
                                  req->proto, req->host, uristr,
                                  querystr.len, querystr.data);
    aos_str_set(signed_url, signed_url_str);

    return res;
}

int get_oss_rtmp_request_signature(const oss_request_options_t *options,
                                   aos_http_request_t *req,
                                   const aos_string_t *expires,
                                   aos_string_t *signature)
{
    aos_string_t canon_res;
    char canon_buf[AOS_MAX_URI_LEN];
    const char *value;
    aos_string_t signstr;
    int res = AOSE_OK;
    int b64Len;
    unsigned char hmac[20];
    char b64[((20 + 1) * 4) / 3];

    canon_res.data = canon_buf;
    canon_res.len = apr_snprintf(canon_buf, sizeof(canon_buf), "/%s", req->resource);

    if ((res = oss_get_rtmp_string_to_sign(options->pool, expires, &canon_res,
        req->query_params, &signstr))!= AOSE_OK) {
        return res;
    }

    HMAC_SHA1(hmac, (unsigned char *)options->config->access_key_secret.data,
              options->config->access_key_secret.len,
              (unsigned char *)signstr.data, signstr.len);

    b64Len = aos_base64_encode(hmac, 20, b64);
    value = apr_psprintf(options->pool, "%.*s", b64Len, b64);
    aos_str_set(signature, value);

    return res;
}

int oss_get_rtmp_string_to_sign(aos_pool_t *p,
                                const aos_string_t *expires,
                                const aos_string_t *canon_res,
                                const aos_table_t *params,
                                aos_string_t *signstr)
{
    int res;
    aos_buf_t *signbuf;
    aos_str_null(signstr);

    signbuf = aos_create_buf(p, 1024);

    // expires
    aos_buf_append_string(p, signbuf, expires->data, expires->len);
    aos_buf_append_string(p, signbuf, "\n", sizeof("\n")-1);

    // canonicalized params
    if ((res = oss_get_canonicalized_params(p, params, signbuf)) != AOSE_OK) {
        return res;
    }

    // canonicalized resource
    aos_buf_append_string(p, signbuf, canon_res->data, canon_res->len);

    // result
    signstr->data = (char *)signbuf->pos;
    signstr->len = aos_buf_size(signbuf);

    return AOSE_OK;
}

static int oss_get_canonicalized_params(aos_pool_t *p,
                                        const aos_table_t *params,
                                        aos_buf_t *signbuf)
{
    int pos;
    int meta_count = 0;
    int i;
    int len;
    const aos_array_header_t *tarr;
    const aos_table_entry_t *telts;
    char **meta_headers;
    const char *value;
    aos_string_t tmp_str;
    char *tmpbuf = (char*)malloc(AOS_MAX_HEADER_LEN + 1);
    if (NULL == tmpbuf) {
        aos_error_log("malloc %d memory failed.", AOS_MAX_HEADER_LEN + 1);
        return AOSE_OVER_MEMORY;
    }

    if (apr_is_empty_table(params)) {
        free(tmpbuf);
        return AOSE_OK;
    }

    // sort user meta header
    tarr = aos_table_elts(params);
    telts = (aos_table_entry_t*)tarr->elts;
    meta_headers = aos_pcalloc(p, tarr->nelts * sizeof(char*));
    for (pos = 0; pos < tarr->nelts; ++pos) {
        aos_string_t key = aos_string(telts[pos].key);
        meta_headers[meta_count++] = key.data;
    }
    if (meta_count == 0) {
        free(tmpbuf);
        return AOSE_OK;
    }
    aos_gnome_sort((const char **)meta_headers, meta_count);

    // sign string
    for (i = 0; i < meta_count; ++i) {
        value = apr_table_get(params, meta_headers[i]);
        aos_str_set(&tmp_str, value);
        aos_strip_space(&tmp_str);
        len = apr_snprintf(tmpbuf, AOS_MAX_HEADER_LEN + 1, "%s:%.*s",
                           meta_headers[i], tmp_str.len, tmp_str.data);
        if (len > AOS_MAX_HEADER_LEN) {
            free(tmpbuf);
            aos_error_log("rtmp parameters too many, %d > %d.",
                          len, AOS_MAX_HEADER_LEN);
            return AOSE_INVALID_ARGUMENT;
        }
        tmp_str.data = tmpbuf;
        tmp_str.len = len;
        aos_buf_append_string(p, signbuf, tmpbuf, len);
        aos_buf_append_string(p, signbuf, "\n", sizeof("\n")-1);
    }

    free(tmpbuf);
    return AOSE_OK;
}
