/* Copyright 2007 Takayuki Ogiso
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <string.h>

#include "httpd.h"
#include "http_config.h"
#include "http_protocol.h"
#include "http_log.h"
#include "ap_config.h"
#include "apr_strings.h"
#include "apr_date.h"
#include "apr_time.h"
#include "apr_sha1.h"
#include "apr_base64.h"

#define DEFAULT_COOKIE_NAME "auth_hmac"
#define SEPARATE_CHAR '\t'
#define BLOCK_SIZE 64

module AP_MODULE_DECLARE_DATA auth_hmac_module;

typedef struct {
    char *key;
    char *cookie_name;
    char *redirect_url;
} auth_hmac_dir_conf;

static char *get_cookie_value(request_rec *r, const char *cookie_name)
{
    const char *cookies;
    
    if (cookies = apr_table_get(r->headers_in, "Cookie")) {
        const char *start, *end;
        char *name = apr_pstrcat(r->pool, cookie_name, "=", NULL);
        start = cookies;
        while ((start = ap_strstr_c(start, name)) != NULL) {
            if (start == cookies || start[-1] == ' ' ||
                start[-1] == ';' || start[-1] == ',') {
                start += strlen(name);
                end = start;
                size_t length = strlen(start);
                while (*end != '\0') {
                    if (*end == ';' || *end == ',') {
                        length = end - start;
                        break;
                    }
                    end++;
                }
                char *cookie = apr_pstrndup(r->pool, start, length);
                char *plus_cookie = cookie;
                while (*plus_cookie != '\0') {
                    if (*plus_cookie == '+') {
                        *plus_cookie = ' ';
                    }
                    plus_cookie++;
                }
                if (ap_unescape_url(cookie) == OK) {
                    return cookie;
                }
                return NULL;
            }
            start++;
        }
    }
    return NULL;
}

static char *get_expire_string(request_rec *r, const char *cookie_value)
{
    size_t i = 0;
    while (cookie_value[i] != '\0') {
        if (cookie_value[i] == SEPARATE_CHAR) {
            char *expire_string = apr_pstrndup(r->pool, cookie_value, i);
            return expire_string;
        }
        i++;
    }
    return NULL;
}

static char *get_hmac_string(request_rec *r, const char *cookie_value)
{
    char separate_char[2] = {SEPARATE_CHAR, '\0'};
    char *second_part = ap_strstr(cookie_value, separate_char);
    if (second_part != NULL) {
        char *hmac_string = apr_pstrdup(r->pool, second_part + 1);
        return hmac_string;
    }
    return NULL;
}

static int get_auth_status(request_rec *r, const char *redirect_url)
{
	if (redirect_url == NULL) {
		return HTTP_FORBIDDEN;
	} else {
		apr_table_set(r->headers_out, "Location", redirect_url);
		return HTTP_MOVED_TEMPORARILY;
	}
}

#define HEX_CHAR "0123456789abcdef"
static size_t uchar_to_hex(unsigned char *bin, size_t bin_size, char *hex, size_t hex_length)
{
    size_t i, j = 0;
    for (i = 0; i < bin_size; i++) {
        *(hex + j++) = HEX_CHAR[(bin[i] >> 4 & (unsigned int)0xf)];
        if (j > hex_length) {
            return hex_length;
        }
        *(hex + j++) = HEX_CHAR[bin[i] & (unsigned int)0xf];
        if (j > hex_length) {
            return hex_length;
        }
    }
    return j - 1;
}

static char *get_hmac_sha1(request_rec *r, const char *key, const char *expire)
{
    unsigned char ipad[BLOCK_SIZE];
    unsigned char opad[BLOCK_SIZE];
    unsigned char hmac[APR_SHA1_DIGESTSIZE];
    char hmac_hex[APR_SHA1_DIGESTSIZE * 2];
    struct apr_sha1_ctx_t context;
    
    unsigned char *ukey = (unsigned char *)apr_palloc(r->pool, apr_base64_decode_len(key));
    int key_length = apr_base64_decode_binary(ukey, key);
    
    if (key_length > BLOCK_SIZE) {
        unsigned char hmac_key[APR_SHA1_DIGESTSIZE];
        struct apr_sha1_ctx_t key_context;
        
        apr_sha1_init(&key_context);
        apr_sha1_update_binary(&key_context, ukey, key_length);
        apr_sha1_final(hmac_key, &key_context);
        
        ukey = hmac_key;
        key_length = APR_SHA1_DIGESTSIZE;
    }
    
    memset(ipad, 0, BLOCK_SIZE);
    memset(opad, 0, BLOCK_SIZE);
    memcpy(ipad, ukey, key_length);
    memcpy(opad, ukey, key_length);
    
    int i;
    for (i = 0; i < BLOCK_SIZE; i++) {
        ipad[i] ^= 0x36;
        opad[i] ^= 0x5c;
    }
    
    apr_sha1_init(&context);
    apr_sha1_update_binary(&context, ipad, BLOCK_SIZE);
    apr_sha1_update(&context, expire, strlen(expire));
    apr_sha1_final(hmac, &context);
    
    apr_sha1_init(&context);
    apr_sha1_update_binary(&context, opad, BLOCK_SIZE);
    apr_sha1_update_binary(&context, hmac, APR_SHA1_DIGESTSIZE);
    apr_sha1_final(hmac, &context);
    
    uchar_to_hex(hmac, APR_SHA1_DIGESTSIZE, hmac_hex, sizeof(hmac_hex));
    char *phmac = apr_pstrndup(r->pool, hmac_hex, sizeof(hmac_hex));
    return phmac;
}

static int validate(request_rec *r, const char *key, const char *cookie_value)
{
    char *expire_string = get_expire_string(r, cookie_value);
    if (expire_string == NULL) {
        return 16;
    }
    apr_time_t expire = apr_date_parse_http(expire_string);
    if (r->request_time > expire) {
        return 32;
    }
    
    char *hmac_string = get_hmac_string(r, cookie_value);
    if (hmac_string == NULL) {
        return 256;
    }
    
    char *expect_hmac = get_hmac_sha1(r, key, expire_string);
    if (expect_hmac == NULL) {
        return 512;
    }
    
    if (strcasecmp(hmac_string, expect_hmac) != 0) {
        return 1;
    }
    
    return 0;
}

static int auth_hmac(request_rec *r)
{
    auth_hmac_dir_conf *conf =
        (auth_hmac_dir_conf *)ap_get_module_config(r->per_dir_config, &auth_hmac_module);
    
    if (conf->key == NULL) {
        return DECLINED;
    }
    
    char *cookie = get_cookie_value(r, conf->cookie_name);
    if (cookie == NULL) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                      "mod_auth_hmac: authentication failed");
		return get_auth_status(r, conf->redirect_url);
    }
    
    int auth_result = validate(r, conf->key, cookie);
    if (auth_result != 0) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                      "mod_auth_hmac: authentication failed [%d]", auth_result);
        return get_auth_status(r, conf->redirect_url);
    }
    
    return OK;
}

static void *create_dir_config(apr_pool_t *p, char *dir)
{
    auth_hmac_dir_conf *conf =
        (auth_hmac_dir_conf *)apr_palloc(p, sizeof(auth_hmac_dir_conf));
    
    conf->key          = NULL;
    conf->cookie_name  = DEFAULT_COOKIE_NAME;
    conf->redirect_url = NULL;
    
    return conf;
}

static const char *set_key(cmd_parms *cmd, void *config, const char *key)
{
    auth_hmac_dir_conf *conf = (auth_hmac_dir_conf *)config;
    conf->key = apr_pstrdup(cmd->pool, key);
    return NULL;
}

static const char *set_cookie_name(cmd_parms *cmd, void *config, const char *name)
{
    auth_hmac_dir_conf *conf = (auth_hmac_dir_conf *)config;
    conf->cookie_name = apr_pstrdup(cmd->pool, name);
    return NULL;
}

static const char *set_redirect_url(cmd_parms *cmd, void *config, const char *url)
{
    auth_hmac_dir_conf *conf = (auth_hmac_dir_conf *)config;
    conf->redirect_url = apr_pstrdup(cmd->pool, url);
    return NULL;
}

static void auth_hmac_register_hooks(apr_pool_t *p)
{
    ap_hook_access_checker(auth_hmac, NULL, NULL, APR_HOOK_MIDDLE);
}

static const command_rec auth_hmac_cmds[] = {
    AP_INIT_TAKE1("AuthHmac_Key", set_key, NULL, OR_AUTHCFG,
                  "HMAC key (BASE64 encoded)"),
    AP_INIT_TAKE1("AuthHmac_CookieName", set_cookie_name, NULL, OR_AUTHCFG,
                  "Cookie name"),
    AP_INIT_TAKE1("AuthHmac_LoginURL", set_redirect_url, NULL, OR_AUTHCFG,
                  "Login page URL"),
    {NULL}
};

/* Dispatch list for API hooks */
module AP_MODULE_DECLARE_DATA auth_hmac_module = {
    STANDARD20_MODULE_STUFF, 
    create_dir_config,        /* create per-dir    config structures */
    NULL,                     /* merge  per-dir    config structures */
    NULL,                     /* create per-server config structures */
    NULL,                     /* merge  per-server config structures */
    auth_hmac_cmds,           /* table of config file commands       */
    auth_hmac_register_hooks  /* register hooks                      */
};

