/*
 * Copyright (c) 2012-2019 Belledonne Communications SARL.
 *
 * This file is part of belle-sip.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

#include "bctoolbox/crypto.h"
#include "belle-sip/auth-helper.h"
#include "belle_sip_internal.h"
#include <string.h>

#ifndef BELLE_SIP_CNONCE_LENGTH
#define BELLE_SIP_CNONCE_LENGTH 16
#endif

#define CHECK_IS_PRESENT(obj, header_name, name)                                                                       \
	if (!belle_sip_header_##header_name##_get_##name(obj)) {                                                           \
		belle_sip_error("parameter [" #name "] not found for header [" #header_name "]");                              \
		return -1;                                                                                                     \
	}

static void belle_sip_auth_helper_clone_authorization(belle_sip_header_authorization_t *authorization,
                                                      const belle_sip_header_www_authenticate_t *authentication) {
	CLONE_STRING_GENERIC(belle_sip_header_www_authenticate, belle_sip_header_authorization, scheme, authorization,
	                     authentication)
	CLONE_STRING_GENERIC(belle_sip_header_www_authenticate, belle_sip_header_authorization, realm, authorization,
	                     authentication)
	CLONE_STRING_GENERIC(belle_sip_header_www_authenticate, belle_sip_header_authorization, nonce, authorization,
	                     authentication)
	CLONE_STRING_GENERIC(belle_sip_header_www_authenticate, belle_sip_header_authorization, algorithm, authorization,
	                     authentication)
	CLONE_STRING_GENERIC(belle_sip_header_www_authenticate, belle_sip_header_authorization, opaque, authorization,
	                     authentication)
}

static void belle_sip_auth_helper_clone_www_authenticate(belle_sip_header_www_authenticate_t *authentication,
                                                         const belle_sip_header_authorization_t *authorization){
    CLONE_STRING_GENERIC(
        belle_sip_header_authorization, belle_sip_header_www_authenticate, scheme, authentication, authorization)
        CLONE_STRING_GENERIC(
            belle_sip_header_authorization, belle_sip_header_www_authenticate, realm, authentication, authorization)
            CLONE_STRING_GENERIC(
                belle_sip_header_authorization, belle_sip_header_www_authenticate, nonce, authentication, authorization)
                CLONE_STRING_GENERIC(belle_sip_header_authorization,
                                     belle_sip_header_www_authenticate,
                                     algorithm,
                                     authentication,
                                     authorization) CLONE_STRING_GENERIC(belle_sip_header_authorization,
                                                                         belle_sip_header_www_authenticate,
                                                                         opaque,
                                                                         authentication,
                                                                         authorization)}

belle_sip_header_authorization_t *belle_sip_auth_helper_create_authorization(
    const belle_sip_header_www_authenticate_t *authentication) {
	belle_sip_header_authorization_t *authorization = belle_sip_header_authorization_new();
	belle_sip_auth_helper_clone_authorization(authorization, authentication);
	return authorization;
}

belle_sip_header_www_authenticate_t *
belle_sip_auth_helper_create_www_authenticate(const belle_sip_header_authorization_t *authorization) {
	belle_sip_header_www_authenticate_t *www_authenticate = belle_sip_header_www_authenticate_new();
	belle_sip_auth_helper_clone_www_authenticate(www_authenticate, authorization);
	return www_authenticate;
}

belle_http_header_authorization_t *
belle_http_auth_helper_create_authorization(const belle_sip_header_www_authenticate_t *authentication) {
	belle_http_header_authorization_t *authorization = belle_http_header_authorization_new();
	belle_sip_auth_helper_clone_authorization(BELLE_SIP_HEADER_AUTHORIZATION(authorization), authentication);
	return authorization;
}

belle_sip_header_proxy_authorization_t *
belle_sip_auth_helper_create_proxy_authorization(const belle_sip_header_proxy_authenticate_t *proxy_authentication) {
	belle_sip_header_proxy_authorization_t *authorization = belle_sip_header_proxy_authorization_new();
	belle_sip_auth_helper_clone_authorization(BELLE_SIP_HEADER_AUTHORIZATION(authorization),
	                                          BELLE_SIP_HEADER_WWW_AUTHENTICATE(proxy_authentication));
	return authorization;
}

static void belle_sip_auth_choose_method(const char *algo, const char *ask, uint8_t *out, size_t size) {
	if ((algo == NULL) || (!strcasecmp(algo, "MD5"))) {
		// By default, using MD5 when algorithm is NULL
		bctbx_md5((const uint8_t *)ask, strlen(ask), out);
	} else if (!strcasecmp(algo, "SHA-256")) {
		bctbx_sha256((const uint8_t *)ask, strlen(ask), (uint8_t)size, out);
	}
}

int belle_sip_auth_define_size(const char *algo) {
	if (algo == NULL || strcasecmp(algo, "MD5") == 0) {
		return 33;
	} else if (strcasecmp(algo, "SHA-256") == 0) {
		return 65;
	} else {
		return 0;
	}
}

int belle_sip_auth_helper_compute_ha1_for_algorithm(
    const char *userid, const char *realm, const char *password, char *ha1, size_t size, const char *algo) {
	size_t compared_size;
	compared_size = belle_sip_auth_define_size(algo);
	if (compared_size != size) {
		belle_sip_error("belle_sip_fill_authorization_header, size of ha1 must be 33 when MD5 or 65 when SHA-256 ");
		return -1;
	}
	size_t length_byte = (size - 1) / 2;
	uint8_t out[MAX_LENGTH_BYTE];
	size_t di;
	char *ask;
	if (!userid) {
		belle_sip_error("belle_sip_fill_authorization_header, username not found ");
		return -1;
	}
	if (!password) {
		belle_sip_error("belle_sip_fill_authorization_header, password not found ");
		return -1;
	}
	if (!realm) {
		belle_sip_error("belle_sip_fill_authorization_header, realm not found ");
		return -1;
	}

	ask = bctbx_strdup_printf("%s:%s:%s", userid, realm, password);
	belle_sip_auth_choose_method(algo, ask, out, length_byte);
	for (di = 0; di < length_byte; ++di)
		sprintf(ha1 + di * 2, "%02x", out[di]);
	ha1[length_byte * 2] = '\0';
	bctbx_free(ask);
	return 0;
}

int belle_sip_auth_helper_compute_ha1(const char *userid, const char *realm, const char *password, char ha1[33]) {
	belle_sip_auth_helper_compute_ha1_for_algorithm(userid, realm, password, ha1, 33, "MD5");
	return 0;
}

int belle_sip_auth_helper_compute_ha2_for_algorithm(
    const char *method, const char *uri, char *ha2, size_t size, const char *algo) {
	size_t compared_size;
	compared_size = belle_sip_auth_define_size(algo);
	if (compared_size != size) {
		belle_sip_error("belle_sip_fill_authorization_header, size of ha1 must be 33 when MD5 or 65 when SHA-256 ");
		return -1;
	}
	size_t length_byte = (size - 1) / 2;
	uint8_t out[MAX_LENGTH_BYTE];
	size_t di;
	char *ask;
	ha2[length_byte * 2] = '\0';
	/*HA2=MD5(method:uri)*/

	ask = bctbx_strdup_printf("%s:%s", method, uri);
	belle_sip_auth_choose_method(algo, ask, out, length_byte);
	for (di = 0; di < length_byte; ++di)
		sprintf(ha2 + di * 2, "%02x", out[di]);
	bctbx_free(ask);
	return 0;
}

int belle_sip_auth_helper_compute_ha2(const char *method, const char *uri, char ha2[33]) {
	belle_sip_auth_helper_compute_ha2_for_algorithm(method, uri, ha2, 33, "MD5");
	return 0;
}

int belle_sip_auth_helper_compute_response_for_algorithm(
    const char *ha1, const char *nonce, const char *ha2, char *response, size_t size, const char *algo) {
	size_t compared_size;
	compared_size = belle_sip_auth_define_size(algo);
	if (compared_size != size) {
		belle_sip_error("belle_sip_fill_authorization_header, size of ha1 must be 33 when MD5 or 65 when SHA-256 ");
		return -1;
	}
	size_t length_byte = (size - 1) / 2;
	uint8_t out[MAX_LENGTH_BYTE];
	size_t di;
	char *ask;
	response[length_byte * 2] = '\0';

	ask = bctbx_strdup_printf("%s:%s:%s", ha1, nonce, ha2);
	belle_sip_auth_choose_method(algo, ask, out, length_byte);
	/*copy values*/
	for (di = 0; di < length_byte; ++di)
		sprintf(response + di * 2, "%02x", out[di]);
	bctbx_free(ask);
	return 0;
}

int belle_sip_auth_helper_compute_response(const char *ha1, const char *nonce, const char *ha2, char response[33]) {
	belle_sip_auth_helper_compute_response_for_algorithm(ha1, nonce, ha2, response, 33, "MD5");
	return 0;
}

int belle_sip_auth_helper_compute_response_qop_auth_for_algorithm(const char *ha1,
                                                                  const char *nonce,
                                                                  unsigned int nonce_count,
                                                                  const char *cnonce,
                                                                  const char *qop,
                                                                  const char *ha2,
                                                                  char *response,
                                                                  size_t size,
                                                                  const char *algo) {
	size_t compared_size;
	compared_size = belle_sip_auth_define_size(algo);
	if (compared_size != size) {
		belle_sip_error("belle_sip_fill_authorization_header, size of ha1 must be 33 when MD5 or 65 when SHA-256 ");
		return -1;
	}
	size_t length_byte = (size - 1) / 2;
	uint8_t out[MAX_LENGTH_BYTE];
	size_t di;
	char *ask;
	char nounce_count_as_string[9];

	response[length_byte * 2] = '\0';

	snprintf(nounce_count_as_string, sizeof(nounce_count_as_string), "%08x", nonce_count);
	/*response=MD5(HA1:nonce:nonce_count:cnonce:qop:HA2)*/

	ask = bctbx_strdup_printf("%s:%s:%s:%s:%s:%s", ha1, nonce, nounce_count_as_string, cnonce, qop, ha2);
	belle_sip_auth_choose_method(algo, ask, out, length_byte);
	/*copy values*/
	for (di = 0; di < length_byte; ++di)
		sprintf(response + di * 2, "%02x", out[di]);
	bctbx_free(ask);
	return 0;
}

int belle_sip_auth_helper_compute_response_qop_auth(const char *ha1,
                                                    const char *nonce,
                                                    unsigned int nonce_count,
                                                    const char *cnonce,
                                                    const char *qop,
                                                    const char *ha2,
                                                    char response[33]) {
	belle_sip_auth_helper_compute_response_qop_auth_for_algorithm(ha1, nonce, nonce_count, cnonce, qop, ha2, response,
	                                                              33, "MD5");
	return 0;
}

int belle_sip_auth_helper_fill_authorization(belle_sip_header_authorization_t *authorization,
                                             const char *method,
                                             const char *ha1) {
	const char *algo = belle_sip_header_authorization_get_algorithm(authorization);
	size_t size = belle_sip_auth_define_size(algo);
	if (!size) {
		belle_sip_error("Algorithm [%s] is not supported ", algo);
		return -1;
	}
	int auth_mode = 0;
	char *uri;
	char ha2[MAX_RESPONSE_SIZE];
	char response[MAX_RESPONSE_SIZE];
	char cnonce[BELLE_SIP_CNONCE_LENGTH + 1];

	response[size - 1] = ha2[size - 1] = '\0';

	if (belle_sip_header_authorization_get_scheme(authorization) != NULL &&
	    strcasecmp("Digest", belle_sip_header_authorization_get_scheme(authorization)) != 0) {
		belle_sip_error("belle_sip_fill_authorization_header, unsupported schema [%s]",
		                belle_sip_header_authorization_get_scheme(authorization));
		return -1;
	}
	if (belle_sip_header_authorization_get_qop(authorization) &&
	    !(auth_mode = strcasecmp("auth", belle_sip_header_authorization_get_qop(authorization)) == 0)) {
		belle_sip_error("belle_sip_fill_authorization_header, unsupported qop [%s], use auth or nothing instead",
		                belle_sip_header_authorization_get_qop(authorization));
		return -1;
	}
	CHECK_IS_PRESENT(authorization, authorization, realm)
	CHECK_IS_PRESENT(authorization, authorization, nonce)
	if (BELLE_SIP_IS_INSTANCE_OF(authorization, belle_http_header_authorization_t)) {
		/*http case*/
		if (!belle_http_header_authorization_get_uri(BELLE_HTTP_HEADER_AUTHORIZATION(authorization))) {
			belle_sip_error("parameter uri not found for http header authorization");
			return -1;
		}
	} else {
		CHECK_IS_PRESENT(authorization, authorization, uri)
	}
	if (auth_mode) {
		CHECK_IS_PRESENT(authorization, authorization, nonce_count)
		if (!belle_sip_header_authorization_get_cnonce(authorization)) {
			belle_sip_header_authorization_set_cnonce(authorization, belle_sip_random_token((cnonce), sizeof(cnonce)));
		}
	}
	if (!method) {
		belle_sip_error("belle_sip_fill_authorization_header, method not found ");
		return -1;
	}

	if (BELLE_SIP_IS_INSTANCE_OF(authorization, belle_http_header_authorization_t)) {
		/*http case*/
		uri = belle_generic_uri_to_string(
		    belle_http_header_authorization_get_uri(BELLE_HTTP_HEADER_AUTHORIZATION(authorization)));
	} else {
		uri = belle_sip_uri_to_string(belle_sip_header_authorization_get_uri(authorization));
	}

	belle_sip_auth_helper_compute_ha2_for_algorithm(method, uri, ha2, size, algo);
	belle_sip_free(uri);
	if (auth_mode) {
		/*response=MD5(HA1:nonce:nonce_count:cnonce:qop:HA2)*/
		belle_sip_auth_helper_compute_response_qop_auth_for_algorithm(
		    ha1, belle_sip_header_authorization_get_nonce(authorization),
		    belle_sip_header_authorization_get_nonce_count(authorization),
		    belle_sip_header_authorization_get_cnonce(authorization),
		    belle_sip_header_authorization_get_qop(authorization), ha2, response, size, algo);
	} else {
		/*response=MD5(ha1:nonce:ha2)*/
		belle_sip_auth_helper_compute_response_for_algorithm(
		    ha1, belle_sip_header_authorization_get_nonce(authorization), ha2, response, size, algo);
	}
	belle_sip_header_authorization_set_response(authorization, (const char *)response);
	return 0;
}

int belle_sip_auth_helper_fill_proxy_authorization(belle_sip_header_proxy_authorization_t *proxy_authorization,
                                                   const char *method,
                                                   const char *ha1) {
	return belle_sip_auth_helper_fill_authorization(BELLE_SIP_HEADER_AUTHORIZATION(proxy_authorization), method, ha1);
}
