/* 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 <time.h>

#include "httpd.h"
#include "http_config.h"
#include "http_protocol.h"
#include "http_log.h"
#include "ap.h"
#include "ap_config.h"
#include "ap_alloc.h"
#include "ap_sha1.h"
#include "util_date.h"

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

module MODULE_VAR_EXPORT 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 = ap_table_get(r->headers_in, "Cookie")) {
        const char *start, *end;
        char *name = ap_pstrcat(r->pool, cookie_name, "=", NULL);
        start = cookies;
        while ((start = strstr(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 = ap_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 = ap_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 = strstr(cookie_value, separate_char);
    if (second_part != NULL) {
        char *hmac_string = ap_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 {
		ap_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[SHA_DIGESTSIZE];
    char hmac_hex[SHA_DIGESTSIZE * 2];
    AP_SHA1_CTX context;
    
    unsigned char *ukey = (unsigned char *)ap_palloc(r->pool, ap_base64decode_len(key));
    int key_length = ap_base64decode_binary(ukey, key);
    
    if (key_length > BLOCK_SIZE) {
        unsigned char hmac_key[SHA_DIGESTSIZE];
        AP_SHA1_CTX key_context;
        
        ap_SHA1Init(&key_context);
        ap_SHA1Update_binary(&key_context, ukey, key_length);
        ap_SHA1Final(hmac_key, &key_context);
        
        ukey = hmac_key;
        key_length = SHA_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;
    }
    
    ap_SHA1Init(&context);
    ap_SHA1Update_binary(&context, ipad, BLOCK_SIZE);
    ap_SHA1Update(&context, expire, strlen(expire));
    ap_SHA1Final(hmac, &context);
    
    ap_SHA1Init(&context);
    ap_SHA1Update_binary(&context, opad, BLOCK_SIZE);
    ap_SHA1Update_binary(&context, hmac, SHA_DIGESTSIZE);
    ap_SHA1Final(hmac, &context);
    
    uchar_to_hex(hmac, SHA_DIGESTSIZE, hmac_hex, sizeof(hmac_hex));
    char *phmac = ap_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;
    }
    time_t expire = ap_parseHTTPdate(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_NOERRNO|APLOG_ERR, 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_NOERRNO|APLOG_ERR, r,
                      "mod_auth_hmac: authentication failed [%d]", auth_result);
        return get_auth_status(r, conf->redirect_url);
    }
    
    return OK;
}

static void *create_dir_config(pool *p, char *dir)
{
    auth_hmac_dir_conf *conf =
        (auth_hmac_dir_conf *)ap_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 = ap_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 = ap_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 = ap_pstrdup(cmd->pool, url);
    return NULL;
}

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

/* Dispatch list for API hooks */
module MODULE_VAR_EXPORT auth_hmac_module = {
    STANDARD_MODULE_STUFF, 
    NULL,                  /* module initializer                  */
    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       */
    NULL,                  /* [#8] MIME-typed-dispatched handlers */
    NULL,                  /* [#1] URI to filename translation    */
    NULL,                  /* [#4] validate user id from request  */
    NULL,                  /* [#5] check if the user is ok _here_ */
    auth_hmac,             /* [#3] check access by host address   */
    NULL,                  /* [#6] determine MIME type            */
    NULL,                  /* [#7] pre-run fixups                 */
    NULL,                  /* [#9] log a transaction              */
    NULL,                  /* [#2] header parser                  */
    NULL,                  /* child_init                          */
    NULL,                  /* child_exit                          */
    NULL                   /* [#0] post read-request              */
#ifdef EAPI
   ,NULL,                  /* EAPI: add_module                    */
    NULL,                  /* EAPI: remove_module                 */
    NULL,                  /* EAPI: rewrite_command               */
    NULL                   /* EAPI: new_connection                */
#endif
};

