/*
** Copyright (C) 2001-2025 Zabbix SIA
**
** This program is free software: you can redistribute it and/or modify it under the terms of
** the GNU Affero General Public License as published by the Free Software Foundation, version 3.
**
** 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 Affero General Public License for more details.
**
** You should have received a copy of the GNU Affero General Public License along with this program.
** If not, see <https://www.gnu.org/licenses/>.
**/

#include "zbxcurl.h"

#ifdef HAVE_LIBCURL

/* See https://curl.se/libcurl/c/symbols-by-name.html for information in which version a symbol was added. */

/* added in 7.85.0 (0x075500) */
#if LIBCURL_VERSION_NUM < 0x075500
#	define CURLOPT_PROTOCOLS_STR	10318L
#endif

static unsigned int	libcurl_version_num(void)
{
	return curl_version_info(CURLVERSION_NOW)->version_num;
}

static const char	*libcurl_version_str(void)
{
	return curl_version_info(CURLVERSION_NOW)->version;
}

static const char	*libcurl_ssl_version(void)
{
	return curl_version_info(CURLVERSION_NOW)->ssl_version;
}

/* curl_multi_wait() was added in cURL 7.28.0 (0x071c00). Since we support cURL library >= 7.19.1  */
/* we want to be able to compile against older cURL library. This is a wrapper that detects if the */
/* function is available at runtime. It should never be called for older library versions because  */
/* detect the version before. When cURL library requirement goes to >= 7.28.0 this function and    */
/* the structure declaration should be removed and curl_multi_wait() be used directly.             */
CURLMcode	zbx_curl_multi_wait(CURLM *multi_handle, int timeout_ms, int *numfds)
{
#if LIBCURL_VERSION_NUM < 0x071c00
	struct curl_waitfd
	{
		curl_socket_t	fd;
		short		events;
		short		revents;
	};
#endif
	static void		*handle;
	static CURLMcode	(*fptr)(CURLM *, struct curl_waitfd *, unsigned int, int, int *) = NULL;

	if (NULL == fptr)
	{
		/* this check must be performed before calling this function */
		if (SUCCEED != zbx_curl_good_for_elasticsearch(NULL))
		{
			zabbix_log(LOG_LEVEL_CRIT, "zbx_curl_multi_wait() should never be called when using cURL library"
					" <= 7.28.0 (using version %s)", libcurl_version_str());
			THIS_SHOULD_NEVER_HAPPEN;
			exit(EXIT_FAILURE);
		}

#ifndef _WINDOWS
		if (NULL == (handle = dlopen(NULL, RTLD_LAZY | RTLD_NOLOAD)))
		{
			zabbix_log(LOG_LEVEL_CRIT, "cannot dlopen() Zabbix binary: %s", dlerror());
			exit(EXIT_FAILURE);
		}

		/* use *(void **)(&fptr) to silence the "-pedantic" warning */
		if (NULL == (*(void **)(&fptr) = dlsym(handle, "curl_multi_wait")))
		{
			zabbix_log(LOG_LEVEL_CRIT, "cannot find cURL function curl_multi_wait(): %s", dlerror());
			dlclose(handle);
			exit(EXIT_FAILURE);
		}
#else
		fptr = curl_multi_wait;
#endif
	}

	return fptr(multi_handle, NULL, 0, timeout_ms, numfds);
}

static const char	*get_content_type(CURL *easyhandle)
{
	char		*content_type = NULL;
	CURLcode	err;

	err = curl_easy_getinfo(easyhandle, CURLINFO_CONTENT_TYPE, &content_type);

	if (CURLE_OK != err || NULL == content_type)
		zabbix_log(LOG_LEVEL_DEBUG, "cannot get content type: %s", curl_easy_strerror(err));
	else
		zabbix_log(LOG_LEVEL_DEBUG, "content_type '%s'", content_type);

	return content_type;
}

/* curl_easy_header() was added in cURL 7.83.0 (0x075300) */
const char	*zbx_curl_content_type(CURL *easyhandle)
{
#if LIBCURL_VERSION_NUM < 0x075300
#	define CURLH_HEADER	(1<<0)
#	define CURLH_TRAILER	(1<<1)
#	define CURLH_CONNECT	(1<<2)
#	define CURLH_1XX	(1<<3)
#	define CURLH_PSEUDO	(1<<4)

	typedef enum
	{
		CURLHE_OK,
		CURLHE_BADINDEX,
		CURLHE_MISSING,
		CURLHE_NOHEADERS,
		CURLHE_NOREQUEST,
		CURLHE_OUT_OF_MEMORY,
		CURLHE_BAD_ARGUMENT,
		CURLHE_NOT_BUILT_IN
	}
	CURLHcode;

	struct curl_header
	{
		char		*name;
		char		*value;
		size_t		amount;
		size_t		index;
		unsigned int	origin;
		void		*anchor;
	};
#endif
	static void		*handle;
	static CURLHcode	(*fptr)(CURL *, const char *, size_t, unsigned int, int, struct curl_header **) = NULL;

	struct curl_header	*type;
	unsigned int		origin;
	CURLHcode		h;

	if (libcurl_version_num() < 0x075300)
	{
		return get_content_type(easyhandle);
	}

	if (NULL == fptr)
	{
#ifndef _WINDOWS
		if (NULL == (handle = dlopen(NULL, RTLD_LAZY | RTLD_NOLOAD)))
		{
			zabbix_log(LOG_LEVEL_CRIT, "cannot dlopen() Zabbix binary: %s", dlerror());
			exit(EXIT_FAILURE);
		}

		/* use *(void **)(&fptr) to silence the "-pedantic" warning */
		if (NULL == (*(void **)(&fptr) = dlsym(handle, "curl_easy_header")))
		{
			zabbix_log(LOG_LEVEL_CRIT, "cannot find cURL function curl_easy_header(): %s", dlerror());
			dlclose(handle);
			exit(EXIT_FAILURE);
		}
#else
		fptr = curl_easy_header;
#endif
	}

	origin = CURLH_HEADER | CURLH_TRAILER | CURLH_CONNECT | CURLH_1XX;

	if (libcurl_version_num() > 0x75400)
	{
		/* the CURLH_PSEUDO wasn't accepted until bug #9235 was fixed in 7.85.0 */
		origin |= CURLH_PSEUDO;
	}

	if (CURLHE_OK != (h = fptr(easyhandle, "Content-Type", 0, origin, -1, &type)))
	{
		if (CURLHE_NOT_BUILT_IN != h)
		{
			zabbix_log(LOG_LEVEL_DEBUG, "cannot retrieve Content-Type header:%u", h);
			return NULL;
		}

		/* the headers API is not compiled in, fall back to the old way of getting it */
		return get_content_type(easyhandle);
	}

	zabbix_log(LOG_LEVEL_DEBUG, "name '%s' value '%s' amount:" ZBX_FS_SIZE_T " index:" ZBX_FS_SIZE_T " origin:%u",
			type->name, type->value, (zbx_fs_size_t)type->amount, (zbx_fs_size_t)type->index, type->origin);

	return type->value;
}

int	zbx_curl_protocol(const char *protocol, char **error)
{
	curl_version_info_data	*ver;
	size_t			index = 0;

	ver = curl_version_info(CURLVERSION_NOW);

	while (NULL != ver->protocols[index])
	{
		if (0 == strcasecmp(protocol, ver->protocols[index]))
			return SUCCEED;

		index++;
	}

	if (NULL != error)
	{
		*error = zbx_dsprintf(*error, "cURL library does not support \"%s\" protocol (using version %s)",
				protocol, libcurl_version_str());
	}

	return FAIL;
}

static void	setopt_error(const char *option, CURLcode err, char **error)
{
	*error = zbx_dsprintf(*error, "cURL library returned error when enabling %s: %s (using version %s)",
			option, curl_easy_strerror(err), libcurl_version_str());
}

int	zbx_curl_setopt_https(CURL *easyhandle, char **error)
{
	CURLcode	err;

/* added in 7.19.4 (0x071304), deprecated since 7.85.0 */
#if LIBCURL_VERSION_NUM < 0x071304
#	define CURLPROTO_HTTP		(1<<0)
#	define CURLPROTO_HTTPS		(1<<1)
#endif

	/* CURLOPT_PROTOCOLS (181L) is supported starting with version 7.19.4 (0x071304) */
	if (libcurl_version_num() >= 0x071304)
	{
		/* CURLOPT_PROTOCOLS was replaced by CURLOPT_PROTOCOLS_STR and deprecated in 7.85.0 (0x075500) */
		if (libcurl_version_num() >= 0x075500)
		{
			if (CURLE_OK != (err = curl_easy_setopt(easyhandle, CURLOPT_PROTOCOLS_STR, "HTTP,HTTPS")))
			{
				setopt_error("HTTP/HTTPS", err, error);
				return FAIL;
			}
		}
		else
		{
			/* 181L is CURLOPT_PROTOCOLS, remove when cURL requirement will become >= 7.85.0 */
			if (CURLE_OK != (err = curl_easy_setopt(easyhandle, 181L, CURLPROTO_HTTP | CURLPROTO_HTTPS)))
			{
				setopt_error("HTTP/HTTPS", err, error);
				return FAIL;
			}
		}
	}

	return SUCCEED;
}

int	zbx_curl_setopt_smtps(CURL *easyhandle, char **error)
{
	CURLcode	err;

/* added in 7.20.0 (0x071400), deprecated since 7.85.0 */
#if LIBCURL_VERSION_NUM < 0x071400
#	define CURLPROTO_SMTP   	(1<<16)
#	define CURLPROTO_SMTPS  	(1<<17)
#endif

	/* CURLOPT_PROTOCOLS (181L) is supported starting with version 7.19.4 (0x071304) */
	if (libcurl_version_num() >= 0x071304)
	{
		/* CURLOPT_PROTOCOLS was replaced by CURLOPT_PROTOCOLS_STR and deprecated in 7.85.0 (0x075500) */
		if (libcurl_version_num() >= 0x075500)
		{
			if (CURLE_OK != (err = curl_easy_setopt(easyhandle, CURLOPT_PROTOCOLS_STR, "SMTP,SMTPS")))
			{
				setopt_error("SMTP/SMTPS", err, error);
				return FAIL;
			}
		}
		else
		{
			/* 181L is CURLOPT_PROTOCOLS, remove when cURL requirement will become >= 7.85.0 */
			if (CURLE_OK != (err = curl_easy_setopt(easyhandle, 181L, CURLPROTO_SMTP | CURLPROTO_SMTPS)))
			{
				setopt_error("SMTP/SMTPS", err, error);
				return FAIL;
			}
		}
	}

	return SUCCEED;
}

int	zbx_curl_setopt_ssl_version(CURL *easyhandle, char **error)
{
	CURLcode	err;

/* CURL_SSLVERSION_TLSv1_2 (6) was added in 7.34.0 (0x072200) */
#if LIBCURL_VERSION_NUM < 0x072200
#	define CURL_SSLVERSION_TLSv1_2	6
#endif

	if (libcurl_version_num() >= 0x072200)
	{
		if (CURLE_OK != (err = curl_easy_setopt(easyhandle, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2)))
		{
			setopt_error("CURL_SSLVERSION_TLSv1_2", err, error);
			return FAIL;
		}
	}

	return SUCCEED;
}

int	zbx_curl_has_bearer(char **error)
{
	/* added in 7.61.0 (0x073d00) */
	if (libcurl_version_num() < 0x073d00)
	{
		if (NULL != error)
		{
			*error = zbx_dsprintf(*error, "cURL library version %s does not support HTTP Bearer token"
					" authentication, 7.61.0 or newer is required", libcurl_version_str());
		}

		return FAIL;
	}

	return SUCCEED;
}

int	zbx_curl_has_ssl(char **error)
{
	if (NULL == libcurl_ssl_version())
	{
		if (NULL != error)
		{
			*error = zbx_dsprintf(*error, "cURL library does not support SSL/TLS (using version %s)",
					libcurl_version_str());
		}

		return FAIL;
	}

	return SUCCEED;
}

int	zbx_curl_has_smtp_auth(char **error)
{
	/* added in 7.20.0 */
	if (libcurl_version_num() < 0x071400)
	{
		if (NULL != error)
		{
			*error = zbx_dsprintf(*error, "cURL library version %s does not support SMTP authentication,"
					" 7.20.0 or newer is required", libcurl_version_str());
		}

		return FAIL;
	}

	return SUCCEED;
}

int	zbx_curl_good_for_elasticsearch(char **error)
{
	/* Elasticsearch needs curl_multi_wait() which was added in 7.28.0 (0x071c00) */
	if (libcurl_version_num() < 0x071c00)
	{
		if (NULL != error)
		{
			*error = zbx_dsprintf(*error, "cURL library version %s is too old for Elasticsearch history"
					" backend, 7.28.0 or newer is required", libcurl_version_str());
		}

		return FAIL;
	}

	return SUCCEED;
}
#endif /* HAVE_LIBCURL */
