/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "randgen.h"
#include "dovecot-openssl-common.h"
#include "iostream-openssl.h"

#include <openssl/ssl.h>
#include <openssl/err.h>
#ifdef HAVE_OSSL_PROVIDER_try_load
#  include <openssl/provider.h>
#else
#  include <openssl/engine.h>
#endif
#include <openssl/rand.h>

static int openssl_init_refcount = 0;
#ifdef HAVE_OSSL_PROVIDER_try_load
static OSSL_PROVIDER *dovecot_openssl_engine = NULL;
#else
static ENGINE *dovecot_openssl_engine = NULL;
#endif

#ifdef HAVE_SSL_NEW_MEM_FUNCS
static void *dovecot_openssl_malloc(size_t size, const char *u0 ATTR_UNUSED, int u1 ATTR_UNUSED)
#else
static void *dovecot_openssl_malloc(size_t size)
#endif
{
	if (size == 0)
		return NULL;
	/* this may be performance critical, so don't use
	   i_malloc() or calloc() */
	void *mem = malloc(size);
	if (unlikely(mem == NULL)) {
		i_fatal_status(FATAL_OUTOFMEM,
			       "OpenSSL: malloc(%zu): Out of memory", size);
	}
	return mem;
}

#ifdef HAVE_SSL_NEW_MEM_FUNCS
static void *dovecot_openssl_realloc(void *ptr, size_t size, const char *u0 ATTR_UNUSED, int u1 ATTR_UNUSED)
#else
static void *dovecot_openssl_realloc(void *ptr, size_t size)
#endif
{
	if (size == 0) {
		free(ptr);
		return NULL;
	}
	void *mem = realloc(ptr, size);
	if (unlikely(mem == NULL)) {
		i_fatal_status(FATAL_OUTOFMEM,
			       "OpenSSL: realloc(%zu): Out of memory", size);
	}
	return mem;
}

#ifdef HAVE_SSL_NEW_MEM_FUNCS
static void dovecot_openssl_free(void *ptr, const char *u0 ATTR_UNUSED, int u1 ATTR_UNUSED)
#else
static void dovecot_openssl_free(void *ptr)
#endif
{
	free(ptr);
}

void dovecot_openssl_common_global_ref(void)
{
	if (openssl_init_refcount++ > 0)
		return;

	/* use our own memory allocation functions that will die instead of
	   returning NULL. this avoids random failures on out-of-memory
	   conditions. */
	if (CRYPTO_set_mem_functions(dovecot_openssl_malloc,
				     dovecot_openssl_realloc, dovecot_openssl_free) == 0) {
		/*i_warning("CRYPTO_set_mem_functions() was called too late");*/
	}

	/* Pass OPENSSL_INIT_NO_ATEXIT to prevent libcrypto from registering
	   its own atexit() handler.  That handler runs after main() returns,
	   which is after Dovecot has dlclose()d plugins that may have
	   registered OpenSSL callbacks (e.g. libcassandra).  Accessing those
	   unmapped code pages from OPENSSL_cleanup() crashes the process.
	   Dovecot calls OPENSSL_cleanup() explicitly via
	   dovecot_openssl_common_global_unref() while the relevant modules
	   are still loaded. */
	OPENSSL_init_ssl(OPENSSL_INIT_NO_ATEXIT, NULL);
}

bool dovecot_openssl_common_global_unref(void)
{
	i_assert(openssl_init_refcount > 0);

	if (--openssl_init_refcount > 0)
		return TRUE;

	if (dovecot_openssl_engine != NULL) {
#ifdef HAVE_OSSL_PROVIDER_try_load
		OSSL_PROVIDER_unload(dovecot_openssl_engine);
#else
		ENGINE_finish(dovecot_openssl_engine);
#endif
		dovecot_openssl_engine = NULL;
	}
	OPENSSL_cleanup();
	return FALSE;
}

int dovecot_openssl_common_global_set_engine(const char *engine,
					     const char **error_r)
{
	if (dovecot_openssl_engine != NULL)
		return 1;

#ifdef HAVE_OSSL_PROVIDER_try_load
	if ((dovecot_openssl_engine = OSSL_PROVIDER_try_load(NULL, engine, 1)) == NULL) {
		*error_r = t_strdup_printf("Cannot load '%s': %s", engine,
					   openssl_iostream_error());
		return 0;
	}
	return 1;
#elif defined(HAVE_ENGINE_by_id)
	ENGINE_load_builtin_engines();
	dovecot_openssl_engine = ENGINE_by_id(engine);
	if (dovecot_openssl_engine == NULL) {
		*error_r = t_strdup_printf("Unknown engine '%s'", engine);
		return 0;
	}
	if (ENGINE_init(dovecot_openssl_engine) == 0) {
		*error_r = t_strdup_printf("ENGINE_init(%s) failed", engine);
		ENGINE_free(dovecot_openssl_engine);
		dovecot_openssl_engine = NULL;
		return -1;
	}
	if (ENGINE_set_default(dovecot_openssl_engine, ENGINE_METHOD_ALL) == 0) {
		*error_r = t_strdup_printf("ENGINE_set_default(%s) failed", engine);
		ENGINE_free(dovecot_openssl_engine);
		dovecot_openssl_engine = NULL;
		return -1;
	}
#else
	*error_r = t_strdup_printf("Cannot load '%s': No engine/provider support available", engine);
#endif
	return 1;
}
