/*
 *  Copyright (C) 2000-2018, Thomas Maier-Komor
 *
 *  This file is part of mbuffer's source code.
 *
 *  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 "mbconf.h"
#include "hashing.h"

#include <assert.h>
#include <dlfcn.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/types.h>

#include "dest.h"
#include "log.h"
#include "globals.h"


#if defined HAVE_LIBMD5 && defined HAVE_MD5_H
#include <md5.h>
#define WITH_LIBMD5

// libmd5 and libssl are xor due to name clashes
#elif defined HAVE_LIBCRYPTO
#include <openssl/md5.h>
#define WITH_SSLMD5
#endif


#define USE_MHASH -3
#define USE_GCRYPT -4
#define USE_LIBMD5 -5
#define USE_SSLMD5 -6
#define USE_RHASH -7

static void *LibMhash = 0, *LibGcrypt = 0, *LibRhash = 0;

typedef struct gcry_md_handle *gcry_md_hd_t;
typedef unsigned int gcry_error_t;
static int (*gcry_md_map_name)(const char *);
static const char *(*gcry_md_algo_name)(int);
static unsigned (*gcry_md_get_algo_dlen)(int);
static gcry_error_t (*gcry_md_open)(gcry_md_hd_t *,int,unsigned);
static unsigned char *(*gcry_md_read)(gcry_md_hd_t,int);
static void (*gcry_md_write)(gcry_md_hd_t, const void *, size_t);

#define MHASH_FAILED ((void*)0)
static void *(*mhash_init)(int);
static void (*mhash_deinit)(void *, void *);
static int (*mhash_count)();
static uint32_t (*mhash_get_block_size)(int);
static const char *(*mhash_get_hash_name_static)(int);
static char (*mhash)(void *,const void *,uint32_t);

typedef void *rhash;
static int (*rhash_count)(void);
static void *(*rhash_init)(unsigned);
static void (*rhash_update)(void *, const void *, size_t);
static void (*rhash_final)(void *, unsigned char *);
static int (*rhash_get_digest_size)(unsigned);
static const char *(*rhash_get_name)(unsigned);


static void initHashLibs()
{
	static int initialized = 0;

	if (initialized)
		return;
	initialized = 1;
	LibMhash = dlopen("libmhash.so",RTLD_NOW);
	if (LibMhash) {
		debugmsg("found libmhash\n");
		mhash_init = (void *(*)(int)) dlsym(LibMhash,"mhash_init");
		mhash_deinit = (void (*)(void *, void *)) dlsym(LibMhash,"mhash_deinit");
		mhash_count = (int (*)()) dlsym(LibMhash,"mhash_count");
		mhash_get_block_size = (uint32_t (*)(int)) dlsym(LibMhash,"mhash_get_block_size");
		mhash_get_hash_name_static = (const char *(*)(int)) dlsym(LibMhash,"mhash_get_hash_name_static");
		mhash = (char (*)(void *,const void*,uint32_t)) dlsym(LibMhash,"mhash");
		if ((mhash_init == 0) || (mhash_deinit == 0) || (mhash_count == 0)
			|| (mhash_get_block_size == 0) || (mhash_get_hash_name_static == 0) || (mhash == 0)) {
			warningmsg("libmhash.so does not contain all required symbols\n");
			dlclose(LibMhash);
			LibMhash = 0;
		}
	}
	LibRhash = dlopen("librhash.so.0",RTLD_NOW);
	if (LibRhash) {
		debugmsg("found librhash\n");
		void (*rhash_library_init)() = (void (*)(void)) dlsym(LibRhash,"rhash_library_init");
		rhash_init = (void *(*)(unsigned)) dlsym(LibRhash,"rhash_init");
		rhash_final = (void (*)(void *, unsigned char*)) dlsym(LibRhash,"rhash_final");
		rhash_count = (int (*)()) dlsym(LibRhash,"rhash_count");
		rhash_get_digest_size = (int (*)(unsigned)) dlsym(LibRhash,"rhash_get_digest_size");
		rhash_get_name = (const char *(*)(unsigned)) dlsym(LibRhash,"rhash_get_name");
		rhash_update = (void (*)(void *,const void*,size_t)) dlsym(LibRhash,"rhash_update");
		if ((rhash_init == 0) || (rhash_final == 0) || (rhash_count == 0)
			|| (rhash_get_digest_size == 0) || (rhash_get_name == 0) || (rhash_update == 0)) {
			warningmsg("librhash.so does not contain all required symbols\n");
			dlclose(LibRhash);
			LibRhash = 0;
		} else {
			rhash_library_init();
		}
	}
	LibGcrypt = dlopen("libgcrypt.so.20",RTLD_NOW);
	if (LibGcrypt) {
		debugmsg("found libmhash\n");
		gcry_md_open = (gcry_error_t (*)(gcry_md_hd_t *,int,unsigned)) dlsym(LibGcrypt,"gcry_md_open");
		gcry_md_read = (unsigned char *(*)(gcry_md_hd_t,int)) dlsym(LibGcrypt,"gcry_md_read");
		gcry_md_write = (void (*)(gcry_md_hd_t,const void *,size_t)) dlsym(LibGcrypt,"gcry_md_write");
		gcry_md_algo_name = (const char *(*)(int)) dlsym(LibGcrypt,"gcry_md_algo_name");
		gcry_md_map_name = (int (*)(const char *)) dlsym(LibGcrypt,"gcry_md_map_name");
		gcry_md_get_algo_dlen = (unsigned (*)(int)) dlsym(LibGcrypt,"gcry_md_get_algo_dlen");
		if ((gcry_md_open == 0) || (gcry_md_read == 0) || (gcry_md_write == 0)
			|| (gcry_md_algo_name == 0) || (gcry_md_map_name == 0) || (gcry_md_get_algo_dlen == 0)) {
			warningmsg("libgcrypt.so.20 does not contain all required symbols\n");
			dlclose(LibGcrypt);
			LibGcrypt = 0;
		}
	}
}


void listHashAlgos()
{
	initHashLibs();
	int n = 0;
	if (LibGcrypt) {
		(void) fprintf(stderr,"valid hash functions of libgcrypt are:\n");
		int algo = 1;
		for (algo = 1; algo < 512; ++algo ) {
			const char *name = gcry_md_algo_name(algo);
			assert(name);
			if (name[0] != '?') {
				assert(algo == gcry_md_map_name(name));
				++n;
				printf("\t%s\n",name);
			}
		}
	}
	if (LibMhash) {
		(void) fprintf(stderr,"valid hash functions of libmhash are:\n");
		int algo = mhash_count();
		while (algo >= 0) {
			const char *algoname = mhash_get_hash_name_static(algo);
			if (algoname) {
				++n;
				(void) fprintf(stderr,"\t%s\n",algoname);
			}
			--algo;
		}
	}
	if (LibRhash) {
		(void) fprintf(stderr,"valid hash functions of librhash are:\n");
		int algo = rhash_count();
		while (algo > 0) {
			const char *algoname = rhash_get_name(algo);
			if (algoname) {
				++n;
				(void) fprintf(stderr,"\t%s\n",algoname);
			}
			--algo;
		}
	}
#ifdef WITH_LIBMD5
	fprintf(stderr,"valid hash functions of libmd5 are:\n\tlibmd5:md5\n");
#endif
#ifdef WITH_SSLMD5
	fprintf(stderr,"valid hash functions of libssl are:\n\topenssl:md5\n");
#endif
	if (n == 0)
		fatal("no hash calculation libraries could be found!\n");
}


static void addDigestDestination(int lib, int algo, const char *algoname)
{
	dest_t *dest = malloc(sizeof(dest_t));
	bzero(dest,sizeof(dest_t));
	dest->name = algoname;
	dest->fd = lib;
	dest->mode = algo;
	if (Dest) {
		dest->next = Dest->next;
		Dest->next = dest;
	} else {
		Dest = dest;
		dest->next = 0;
	}
}


int addHashAlgorithm(const char *name)
{
	initHashLibs();
	if (LibGcrypt) {
		const char *an = name;
		if (0 == memcmp(an,"gcrypt:",7))
			an += 7;
		int algo = gcry_md_map_name(an);
		if (algo != 0) {
			addDigestDestination(USE_GCRYPT,algo,an);
			debugmsg("enabled hash algorithm gcrypt:%s\n",an);
			return 1;
		}
	}
	if (LibMhash) {
		const char *an = name;
		int algo, numalgo = mhash_count();
		if (0 == memcmp(an,"mhash:",6))
			an += 6;
		for (algo = 0; algo <= numalgo; ++algo) {
			const char *algoname = mhash_get_hash_name_static(algo);
			if (algoname && (strcasecmp(algoname,an) == 0)) {
				addDigestDestination(USE_MHASH,algo,an);
				debugmsg("enabled hash algorithm mhash:%s\n",an);
				return 1;
			}
		}
	}
	if (LibRhash) {
		const char *an = name;
		int algo, numalgo = rhash_count();
		if (0 == memcmp(an,"rhash:",6))
			an += 6;
		for (algo = 1; algo <= numalgo; ++algo) {
			const char *algoname = rhash_get_name(algo);
			if (algoname && (strcasecmp(algoname,an) == 0)) {
				addDigestDestination(USE_RHASH,algo,an);
				debugmsg("enabled hash algorithm mhash:%s (%d)\n",an,algo);
				return 1;
			}
		}
	}
#ifdef WITH_LIBMD5
	if ((0 == strcasecmp(name,"md5")) || (0 == strcasecmp(name,"libmd5:md5"))) {
		addDigestDestination(USE_LIBMD5,0,name);
		debugmsg("enabled hash algorithm %s\n",name);
		return 1;
	}
#endif
#ifdef WITH_SSLMD5
	if ((0 == strcasecmp(name,"md5")) || (0 == strcasecmp(name,"openssl:md5"))) {
		addDigestDestination(USE_SSLMD5,0,name);
		debugmsg("enabled hash algorithm %s\n",name);
		return 1;
	}
#endif
	errormsg("invalid or unsupported hash function %s\n",name);
	return 0;
}


void *hashThread(void *arg)
{
	dest_t *dest = (dest_t *) arg;
	gcry_md_hd_t hd;
	void *ctxt = 0;
	int algo = dest->mode;

	switch (dest->fd) {
	case USE_MHASH:
		ctxt = mhash_init(algo);
		assert(ctxt != MHASH_FAILED);
		break;
	case USE_RHASH:
		ctxt = rhash_init(algo);
		assert(ctxt != 0);
		break;
	case USE_GCRYPT:
		gcry_md_open(&hd, dest->mode, 0);
		break;
#ifdef WITH_LIBMD5
	case USE_LIBMD5:
		ctxt = malloc(sizeof(MD5_CTX));
		MD5Init(ctxt);
		break;
#endif
#ifdef WITH_SSLMD5
	case USE_SSLMD5:
		ctxt = malloc(sizeof(MD5_CTX));
		MD5_Init(ctxt);
		break;
#endif
	default:
		abort();
	}

	debugmsg("hashThread(): starting...\n");
	for (;;) {
		int size;

		(void) syncSenders(0,0);
		size = SendSize;
		if (0 == size) {
			size_t ds,al;
			unsigned char hashvalue[128];
			char *msg, *m;
			const char *an;
			int i;
			
			debugmsg("hashThread(): done.\n");
			switch (dest->fd) {
			case USE_GCRYPT:
				ds = gcry_md_get_algo_dlen(dest->mode);
				assert(sizeof(hashvalue) >= ds);
				an = gcry_md_algo_name(dest->mode);
				memcpy(hashvalue,gcry_md_read(hd,dest->mode),ds);
				break;
			case USE_MHASH:
				ds = mhash_get_block_size(algo);
				assert(sizeof(hashvalue) >= ds);
				mhash_deinit(ctxt,hashvalue);
				an =  mhash_get_hash_name_static(algo);
				break;
			case USE_RHASH:
				ds = rhash_get_digest_size(algo);
				assert(sizeof(hashvalue) >= ds);
				rhash_final(ctxt,hashvalue);
				an =  rhash_get_name(algo);
				break;
#ifdef WITH_LIBMD5
			case USE_LIBMD5:
				ds = 16;
				MD5Final(hashvalue,ctxt);
				free(ctxt);
				an = "libmd5:md5";
				break;
#endif
#ifdef WITH_SSLMD5
			case USE_SSLMD5:
				ds = 16;
				MD5_Final(hashvalue,ctxt);
				free(ctxt);
				an = "openssl:md5";
				break;
#endif
			default:
				abort();
			}
			al = strlen(an);
			// 9 = strlen(" hash: ") + \n + \0
			msg = malloc(al+9+(ds<<1));
			assert(msg);
			memcpy(msg,an,al);
			memcpy(msg+al," hash: ",7);
			m = msg + al + 7;
			for (i = 0; i < ds; ++i)
				m += sprintf(m,"%02x",(unsigned int)hashvalue[i]);
			*m++ = '\n';
			*m = 0;
			dest->result = msg;
			pthread_exit((void *) msg);
			return 0;	/* for lint */
		}
		if (Terminate) {
			(void) syncSenders(0,-1);
			infomsg("hashThread(): terminating early upon request...\n");
			pthread_exit((void *) 0);
		}
		debugiomsg("hashThread(): hashing %d@0x%p\n",size,(void*)SendAt);
		switch (dest->fd) {
		case USE_GCRYPT:
			gcry_md_write(hd,SendAt,size);
			break;
		case USE_MHASH:
			mhash(ctxt,SendAt,size);
			break;
		case USE_RHASH:
			rhash_update(ctxt,SendAt,size);
			break;
#ifdef WITH_LIBMD5
		case USE_LIBMD5:
			MD5Update(ctxt,SendAt,size);
			break;
#endif
#ifdef WITH_SSLMD5
		case USE_SSLMD5:
			MD5_Update(ctxt,SendAt,size);
			break;
#endif
		default:
			abort();
		}
	}
	return 0;
}


