#include "./check_curl_helpers.h"
#include <stdbool.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include "../utils.h"
#include "check_curl.d/config.h"
#include "output.h"
#include "perfdata.h"
#include "states.h"

extern int verbose;
char errbuf[MAX_INPUT_BUFFER];
bool is_openssl_callback = false;
bool add_sslctx_verify_fun = false;

check_curl_configure_curl_wrapper
check_curl_configure_curl(const check_curl_static_curl_config config,
						  check_curl_working_state working_state, bool check_cert,
						  bool on_redirect_dependent, int follow_method, long max_depth) {
	check_curl_configure_curl_wrapper result = {
		.errorcode = OK,
		.curl_state =
			{
				.curl_global_initialized = false,
				.curl_easy_initialized = false,
				.curl = NULL,

				.body_buf_initialized = false,
				.body_buf = NULL,
				.header_buf_initialized = false,
				.header_buf = NULL,
				.status_line_initialized = false,
				.status_line = NULL,
				.put_buf_initialized = false,
				.put_buf = NULL,

				.header_list = NULL,
				.host = NULL,
			},
	};

	if ((result.curl_state.status_line = calloc(1, sizeof(curlhelp_statusline))) == NULL) {
		die(STATE_UNKNOWN, "HTTP UNKNOWN - allocation of statusline failed\n");
	}

	if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) {
		die(STATE_UNKNOWN, "HTTP UNKNOWN - curl_global_init failed\n");
	}
	result.curl_state.curl_global_initialized = true;

	if ((result.curl_state.curl = curl_easy_init()) == NULL) {
		die(STATE_UNKNOWN, "HTTP UNKNOWN - curl_easy_init failed\n");
	}
	result.curl_state.curl_easy_initialized = true;

	if (verbose >= 1) {
		handle_curl_option_return_code(
			curl_easy_setopt(result.curl_state.curl, CURLOPT_VERBOSE, 1L), "CURLOPT_VERBOSE");
	}

	/* print everything on stdout like check_http would do */
	handle_curl_option_return_code(curl_easy_setopt(result.curl_state.curl, CURLOPT_STDERR, stdout),
								   "CURLOPT_STDERR");

	if (config.automatic_decompression) {
#if LIBCURL_VERSION_NUM >= MAKE_LIBCURL_VERSION(7, 21, 6)
		handle_curl_option_return_code(
			curl_easy_setopt(result.curl_state.curl, CURLOPT_ACCEPT_ENCODING, ""),
			"CURLOPT_ACCEPT_ENCODING");
#else
		handle_curl_option_return_code(
			curl_easy_setopt(result.curl_state.curl, CURLOPT_ENCODING, ""), "CURLOPT_ENCODING");
#endif /* LIBCURL_VERSION_NUM >= MAKE_LIBCURL_VERSION(7, 21, 6) */
	}

	/* initialize buffer for body of the answer */
	if (curlhelp_initwritebuffer(&result.curl_state.body_buf) < 0) {
		die(STATE_UNKNOWN, "HTTP CRITICAL - out of memory allocating buffer for body\n");
	}
	result.curl_state.body_buf_initialized = true;

	handle_curl_option_return_code(curl_easy_setopt(result.curl_state.curl, CURLOPT_WRITEFUNCTION,
													curlhelp_buffer_write_callback),
								   "CURLOPT_WRITEFUNCTION");
	handle_curl_option_return_code(curl_easy_setopt(result.curl_state.curl, CURLOPT_WRITEDATA,
													(void *)result.curl_state.body_buf),
								   "CURLOPT_WRITEDATA");

	/* initialize buffer for header of the answer */
	if (curlhelp_initwritebuffer(&result.curl_state.header_buf) < 0) {
		die(STATE_UNKNOWN, "HTTP CRITICAL - out of memory allocating buffer for header\n");
	}
	result.curl_state.header_buf_initialized = true;

	handle_curl_option_return_code(curl_easy_setopt(result.curl_state.curl, CURLOPT_HEADERFUNCTION,
													curlhelp_buffer_write_callback),
								   "CURLOPT_HEADERFUNCTION");
	handle_curl_option_return_code(curl_easy_setopt(result.curl_state.curl, CURLOPT_WRITEHEADER,
													(void *)result.curl_state.header_buf),
								   "CURLOPT_WRITEHEADER");

	/* set the error buffer */
	handle_curl_option_return_code(
		curl_easy_setopt(result.curl_state.curl, CURLOPT_ERRORBUFFER, errbuf),
		"CURLOPT_ERRORBUFFER");

	/* set timeouts */
	handle_curl_option_return_code(
		curl_easy_setopt(result.curl_state.curl, CURLOPT_CONNECTTIMEOUT, config.socket_timeout),
		"CURLOPT_CONNECTTIMEOUT");

	handle_curl_option_return_code(
		curl_easy_setopt(result.curl_state.curl, CURLOPT_TIMEOUT, config.socket_timeout),
		"CURLOPT_TIMEOUT");

	/* set proxy */
	/* http(s) proxy can either be given from the command line, or taken from environment variables
	 */
	/* socks4(a) / socks5(h) proxy should be given using the command line */

	/* first source to check is the environment variables */
	/* lower case proxy environment variables are almost always accepted, while some programs also
	checking uppercase ones. discover both, but take the lowercase one if both are present */

	/* extra information: libcurl does not discover the uppercase version HTTP_PROXY due to security
	 * reasons */
	/* https://github.com/curl/curl/blob/d445f2d930ae701039518d695481ee53b8490521/lib/url.c#L1987 */

	/* first environment variable to read is all_proxy. it can be overridden by protocol specific
	 * environment variables */
	char *all_proxy_env = getenv("all_proxy");
	char *all_proxy_uppercase_env = getenv("ALL_PROXY");
	if (all_proxy_env != NULL && strlen(all_proxy_env)) {
		working_state.curlopt_proxy = strdup(all_proxy_env);
		if (all_proxy_uppercase_env != NULL && verbose >= 1) {
			printf("* cURL ignoring environment variable 'ALL_PROXY' as 'all_proxy' is set\n");
		}
	} else if (all_proxy_uppercase_env != NULL && strlen(all_proxy_uppercase_env) > 0) {
		working_state.curlopt_proxy = strdup(all_proxy_uppercase_env);
	}

	/* second environment variable to read is http_proxy. only set curlopt_proxy if ssl is not
	 * toggled */
	char *http_proxy_env = getenv("http_proxy");
	char *http_proxy_uppercase_env = getenv("HTTP_PROXY");
	if (!working_state.use_ssl) {
		if (http_proxy_env != NULL && strlen(http_proxy_env) > 0) {
			working_state.curlopt_proxy = strdup(http_proxy_env);
			if (http_proxy_uppercase_env != NULL && verbose >= 1) {
				printf(
					"* cURL ignoring environment variable 'HTTP_PROXY' as 'http_proxy' is set\n");
			}
		} else if (http_proxy_uppercase_env != NULL && strlen(http_proxy_uppercase_env) > 0) {
			working_state.curlopt_proxy = strdup(http_proxy_uppercase_env);
		}
	}
#ifdef LIBCURL_FEATURE_SSL
	/* optionally read https_proxy environment variable and set curlopt_proxy if ssl is toggled */
	char *https_proxy_env = getenv("https_proxy");
	char *https_proxy_uppercase_env = getenv("HTTPS_PROXY");
	if (working_state.use_ssl) {
		if (https_proxy_env != NULL && strlen(https_proxy_env) > 0) {
			working_state.curlopt_proxy = strdup(https_proxy_env);
			if (https_proxy_uppercase_env != NULL && verbose >= 1) {
				printf(
					"* cURL ignoring environment variable 'HTTPS_PROXY' as 'https_proxy' is set\n");
			}
		} else if (https_proxy_uppercase_env != NULL) {
			working_state.curlopt_proxy = strdup(https_proxy_uppercase_env);
		}
	}
#endif /* LIBCURL_FEATURE_SSL */

	/* second source to check for proxies is command line argument, overwriting the environment
	 * variables */
	if (strlen(config.proxy) > 0) {
		working_state.curlopt_proxy = strdup(config.proxy);
	}

	if (working_state.curlopt_proxy != NULL && strlen(working_state.curlopt_proxy)) {
		handle_curl_option_return_code(
			curl_easy_setopt(result.curl_state.curl, CURLOPT_PROXY, working_state.curlopt_proxy),
			"CURLOPT_PROXY");
		if (verbose >= 1) {
			printf("* curl CURLOPT_PROXY: %s\n", working_state.curlopt_proxy);
		}
	}

	/* set no_proxy */
	/* first source to check is environment variables */
	char *no_proxy_env = getenv("no_proxy");
	char *no_proxy_uppercase_env = getenv("NO_PROXY");
	if (no_proxy_env != NULL && strlen(no_proxy_env)) {
		working_state.curlopt_noproxy = strdup(no_proxy_env);
		if (no_proxy_uppercase_env != NULL && verbose >= 1) {
			printf("* cURL ignoring environment variable 'NO_PROXY' as 'no_proxy' is set\n");
		}
	} else if (no_proxy_uppercase_env != NULL && strlen(no_proxy_uppercase_env) > 0) {
		working_state.curlopt_noproxy = strdup(no_proxy_uppercase_env);
	}

	/* second source to check for no_proxy is command line argument, overwriting the environment
	 * variables */
	if (strlen(config.no_proxy) > 0) {
		working_state.curlopt_noproxy = strdup(config.no_proxy);
	}

	if (working_state.curlopt_noproxy != NULL && strlen(working_state.curlopt_noproxy)) {
		handle_curl_option_return_code(curl_easy_setopt(result.curl_state.curl, CURLOPT_NOPROXY,
														working_state.curlopt_noproxy),
									   "CURLOPT_NOPROXY");
		if (verbose >= 1) {
			printf("* curl CURLOPT_NOPROXY: %s\n", working_state.curlopt_noproxy);
		}
	}

	bool have_local_resolution = hostname_gets_resolved_locally(working_state);
	if (verbose >= 1) {
		printf("* have local name resolution: %s\n", (have_local_resolution ? "true" : "false"));
	}

	/* enable haproxy protocol */
	if (config.haproxy_protocol) {
		handle_curl_option_return_code(
			curl_easy_setopt(result.curl_state.curl, CURLOPT_HAPROXYPROTOCOL, 1L),
			"CURLOPT_HAPROXYPROTOCOL");
	}

	/* fill dns resolve cache to make curl connect to the given server_address instead of the */
	/* host_name, only required for ssl, because we use the host_name later on to make SNI happy */
	char dnscache[DEFAULT_BUFFER_SIZE];
	char addrstr[DEFAULT_BUFFER_SIZE / 2];
	if (working_state.use_ssl && working_state.host_name != NULL && have_local_resolution) {
		char *tmp_mod_address;

		/* lookup_host() requires an IPv6 address without the brackets. */
		if ((strnlen(working_state.server_address, MAX_IPV4_HOSTLENGTH) > 2) &&
			(working_state.server_address[0] == '[')) {
			// Duplicate and strip the leading '['
			tmp_mod_address =
				strndup(working_state.server_address + 1, strlen(working_state.server_address) - 2);
		} else {
			tmp_mod_address = working_state.server_address;
		}

		int res;
		if ((res = lookup_host(tmp_mod_address, addrstr, DEFAULT_BUFFER_SIZE / 2,
							   config.sin_family)) != 0) {
			die(STATE_CRITICAL,
				_("Unable to lookup IP address for '%s': getaddrinfo returned %d - %s"),
				working_state.server_address, res, gai_strerror(res));
		}

		snprintf(dnscache, DEFAULT_BUFFER_SIZE, "%s:%d:%s", working_state.host_name,
				 working_state.serverPort, addrstr);
		result.curl_state.host = curl_slist_append(NULL, dnscache);
		curl_easy_setopt(result.curl_state.curl, CURLOPT_RESOLVE, result.curl_state.host);

		if (verbose >= 1) {
			printf("* curl CURLOPT_RESOLVE: %s\n", dnscache);
		}
	}

	// If server_address is an IPv6 address it must be surround by square brackets
	struct in6_addr tmp_in_addr;
	if (inet_pton(AF_INET6, working_state.server_address, &tmp_in_addr) == 1) {
		char *new_server_address = calloc(strlen(working_state.server_address) + 3, sizeof(char));
		if (new_server_address == NULL) {
			die(STATE_UNKNOWN, "HTTP UNKNOWN - Unable to allocate memory\n");
		}
		snprintf(new_server_address, strlen(working_state.server_address) + 3, "[%s]",
				 working_state.server_address);
		working_state.server_address = new_server_address;
	}

	/* compose URL: use the address we want to connect to, set Host: header later */
	char *url = fmt_url(working_state);

	if (verbose >= 1) {
		printf("* curl CURLOPT_URL: %s\n", url);
	}
	handle_curl_option_return_code(curl_easy_setopt(result.curl_state.curl, CURLOPT_URL, url),
								   "CURLOPT_URL");

	free(url);

	/* extract proxy information for legacy proxy https requests */
	if (!strcmp(working_state.http_method, "CONNECT") ||
		strstr(working_state.server_url, "http") == working_state.server_url) {
		handle_curl_option_return_code(
			curl_easy_setopt(result.curl_state.curl, CURLOPT_PROXY, working_state.server_address),
			"CURLOPT_PROXY");
		handle_curl_option_return_code(curl_easy_setopt(result.curl_state.curl, CURLOPT_PROXYPORT,
														(long)working_state.serverPort),
									   "CURLOPT_PROXYPORT");
		if (verbose >= 2) {
			printf("* curl CURLOPT_PROXY: %s:%d\n", working_state.server_address,
				   working_state.serverPort);
		}
		working_state.http_method = "GET";
		handle_curl_option_return_code(
			curl_easy_setopt(result.curl_state.curl, CURLOPT_URL, working_state.server_url),
			"CURLOPT_URL");
	}

	/* disable body for HEAD request */
	if (working_state.http_method && !strcmp(working_state.http_method, "HEAD")) {
		working_state.no_body = true;
	}

	/* set HTTP protocol version */
	handle_curl_option_return_code(
		curl_easy_setopt(result.curl_state.curl, CURLOPT_HTTP_VERSION, config.curl_http_version),
		"CURLOPT_HTTP_VERSION");

	/* set HTTP method */
	if (working_state.http_method) {
		if (!strcmp(working_state.http_method, "POST")) {
			handle_curl_option_return_code(
				curl_easy_setopt(result.curl_state.curl, CURLOPT_POST, 1L), "CURLOPT_POST");
		} else if (!strcmp(working_state.http_method, "PUT")) {
			handle_curl_option_return_code(
				curl_easy_setopt(result.curl_state.curl, CURLOPT_UPLOAD, 1L), "CURLOPT_UPLOAD");
		} else {
			handle_curl_option_return_code(curl_easy_setopt(result.curl_state.curl,
															CURLOPT_CUSTOMREQUEST,
															working_state.http_method),
										   "CURLOPT_CUSTOMREQUEST");
		}
	}

	char *force_host_header = NULL;
	/* check if Host header is explicitly set in options */
	if (config.http_opt_headers_count) {
		for (size_t i = 0; i < config.http_opt_headers_count; i++) {
			if (strncmp(config.http_opt_headers[i], "Host:", 5) == 0) {
				force_host_header = config.http_opt_headers[i];
			}
		}
	}

	/* set hostname (virtual hosts), not needed if CURLOPT_CONNECT_TO is used, but left in
	 * anyway */
	char http_header[DEFAULT_BUFFER_SIZE];
	if (working_state.host_name != NULL && force_host_header == NULL) {
		if ((working_state.virtualPort != HTTP_PORT && !working_state.use_ssl) ||
			(working_state.virtualPort != HTTPS_PORT && working_state.use_ssl)) {
			snprintf(http_header, DEFAULT_BUFFER_SIZE, "Host: %s:%d", working_state.host_name,
					 working_state.virtualPort);
		} else {
			snprintf(http_header, DEFAULT_BUFFER_SIZE, "Host: %s", working_state.host_name);
		}
		result.curl_state.header_list =
			curl_slist_append(result.curl_state.header_list, http_header);
	}

	/* always close connection, be nice to servers */
	snprintf(http_header, DEFAULT_BUFFER_SIZE, "Connection: close");
	result.curl_state.header_list = curl_slist_append(result.curl_state.header_list, http_header);

	/* attach additional headers supplied by the user */
	/* optionally send any other header tag */
	if (config.http_opt_headers_count) {
		for (size_t i = 0; i < config.http_opt_headers_count; i++) {
			result.curl_state.header_list =
				curl_slist_append(result.curl_state.header_list, config.http_opt_headers[i]);
		}
	}

	/* set HTTP headers */
	handle_curl_option_return_code(
		curl_easy_setopt(result.curl_state.curl, CURLOPT_HTTPHEADER, result.curl_state.header_list),
		"CURLOPT_HTTPHEADER");

#ifdef LIBCURL_FEATURE_SSL
	/* set SSL version, warn about insecure or unsupported versions */
	if (working_state.use_ssl) {
		handle_curl_option_return_code(
			curl_easy_setopt(result.curl_state.curl, CURLOPT_SSLVERSION, config.ssl_version),
			"CURLOPT_SSLVERSION");
	}

	/* client certificate and key to present to server (SSL) */
	if (config.client_cert) {
		handle_curl_option_return_code(
			curl_easy_setopt(result.curl_state.curl, CURLOPT_SSLCERT, config.client_cert),
			"CURLOPT_SSLCERT");
	}

	if (config.client_privkey) {
		handle_curl_option_return_code(
			curl_easy_setopt(result.curl_state.curl, CURLOPT_SSLKEY, config.client_privkey),
			"CURLOPT_SSLKEY");
	}

	if (config.ca_cert) {
		handle_curl_option_return_code(
			curl_easy_setopt(result.curl_state.curl, CURLOPT_CAINFO, config.ca_cert),
			"CURLOPT_CAINFO");
	}

	if (config.ca_cert || config.verify_peer_and_host) {
		/* per default if we have a CA verify both the peer and the
		 * hostname in the certificate, can be switched off later */
		handle_curl_option_return_code(
			curl_easy_setopt(result.curl_state.curl, CURLOPT_SSL_VERIFYPEER, 1L),
			"CURLOPT_SSL_VERIFYPEER");
		handle_curl_option_return_code(
			curl_easy_setopt(result.curl_state.curl, CURLOPT_SSL_VERIFYHOST, 2L),
			"CURLOPT_SSL_VERIFYHOST");
	} else {
		/* backward-compatible behaviour, be tolerant in checks
		 * TODO: depending on more options have aspects we want
		 * to be less tolerant about ssl verfications
		 */
		handle_curl_option_return_code(
			curl_easy_setopt(result.curl_state.curl, CURLOPT_SSL_VERIFYPEER, 0L),
			"CURLOPT_SSL_VERIFYPEER");
		handle_curl_option_return_code(
			curl_easy_setopt(result.curl_state.curl, CURLOPT_SSL_VERIFYHOST, 0L),
			"CURLOPT_SSL_VERIFYHOST");
	}

	/* detect SSL library used by libcurl */
	curlhelp_ssl_library ssl_library = curlhelp_get_ssl_library();

	/* try hard to get a stack of certificates to verify against */
	if (check_cert) {
#	if LIBCURL_VERSION_NUM >= MAKE_LIBCURL_VERSION(7, 19, 1)
		/* inform curl to report back certificates */
		switch (ssl_library) {
		case CURLHELP_SSL_LIBRARY_OPENSSL:
		case CURLHELP_SSL_LIBRARY_LIBRESSL:
			/* set callback to extract certificate with OpenSSL context function (works with
			 * OpenSSL-style libraries only!) */
#		ifdef MOPL_USE_OPENSSL
			/* libcurl and monitoring plugins built with OpenSSL, good */
			add_sslctx_verify_fun = true;
			is_openssl_callback = true;
#		endif /* MOPL_USE_OPENSSL */
			/* libcurl is built with OpenSSL, monitoring plugins, so falling
			 * back to manually extracting certificate information */
			handle_curl_option_return_code(
				curl_easy_setopt(result.curl_state.curl, CURLOPT_CERTINFO, 1L), "CURLOPT_CERTINFO");
			break;

		case CURLHELP_SSL_LIBRARY_NSS:
#		if LIBCURL_VERSION_NUM >= MAKE_LIBCURL_VERSION(7, 34, 0)
			/* NSS: support for CERTINFO is implemented since 7.34.0 */
			handle_curl_option_return_code(
				curl_easy_setopt(result.curl_state.curl, CURLOPT_CERTINFO, 1L), "CURLOPT_CERTINFO");
#		else  /* LIBCURL_VERSION_NUM >= MAKE_LIBCURL_VERSION(7, 34, 0) */
			die(STATE_CRITICAL,
				"HTTP CRITICAL - Cannot retrieve certificates (libcurl linked with SSL library "
				"'%s' is too old)\n",
				curlhelp_get_ssl_library_string(ssl_library));
#		endif /* LIBCURL_VERSION_NUM >= MAKE_LIBCURL_VERSION(7, 34, 0) */
			break;

		case CURLHELP_SSL_LIBRARY_GNUTLS:
#		if LIBCURL_VERSION_NUM >= MAKE_LIBCURL_VERSION(7, 42, 0)
			/* GnuTLS: support for CERTINFO is implemented since 7.42.0 */
			handle_curl_option_return_code(
				curl_easy_setopt(result.curl_state.curl, CURLOPT_CERTINFO, 1L), "CURLOPT_CERTINFO");
#		else  /* LIBCURL_VERSION_NUM >= MAKE_LIBCURL_VERSION(7, 42, 0) */
			die(STATE_CRITICAL,
				"HTTP CRITICAL - Cannot retrieve certificates (libcurl linked with SSL library "
				"'%s' is too old)\n",
				curlhelp_get_ssl_library_string(ssl_library));
#		endif /* LIBCURL_VERSION_NUM >= MAKE_LIBCURL_VERSION(7, 42, 0) */
			break;

		case CURLHELP_SSL_LIBRARY_UNKNOWN:
		default:
			die(STATE_CRITICAL,
				"HTTP CRITICAL - Cannot retrieve certificates (unknown SSL library '%s', must "
				"implement first)\n",
				curlhelp_get_ssl_library_string(ssl_library));
			break;
		}
#	else  /* LIBCURL_VERSION_NUM >= MAKE_LIBCURL_VERSION(7, 19, 1) */
		/* old libcurl, our only hope is OpenSSL, otherwise we are out of luck */
		if (ssl_library == CURLHELP_SSL_LIBRARY_OPENSSL ||
			ssl_library == CURLHELP_SSL_LIBRARY_LIBRESSL) {
			add_sslctx_verify_fun = true;
		} else {
			die(STATE_CRITICAL, "HTTP CRITICAL - Cannot retrieve certificates (no "
								"CURLOPT_SSL_CTX_FUNCTION, no OpenSSL library or libcurl "
								"too old and has no CURLOPT_CERTINFO)\n");
		}
#	endif /* LIBCURL_VERSION_NUM >= MAKE_LIBCURL_VERSION(7, 19, 1) */
	}

#	if LIBCURL_VERSION_NUM >=                                                                     \
		MAKE_LIBCURL_VERSION(7, 10, 6) /* required for CURLOPT_SSL_CTX_FUNCTION */
	// ssl ctx function is not available with all ssl backends
	if (curl_easy_setopt(result.curl_state.curl, CURLOPT_SSL_CTX_FUNCTION, NULL) !=
		CURLE_UNKNOWN_OPTION) {
		handle_curl_option_return_code(
			curl_easy_setopt(result.curl_state.curl, CURLOPT_SSL_CTX_FUNCTION, sslctxfun),
			"CURLOPT_SSL_CTX_FUNCTION");
	}
#	endif
#endif /* LIBCURL_FEATURE_SSL */

	/* set default or user-given user agent identification */
	handle_curl_option_return_code(
		curl_easy_setopt(result.curl_state.curl, CURLOPT_USERAGENT, config.user_agent),
		"CURLOPT_USERAGENT");

	/* proxy-authentication */
	if (strcmp(config.proxy_auth, "")) {
		handle_curl_option_return_code(
			curl_easy_setopt(result.curl_state.curl, CURLOPT_PROXYUSERPWD, config.proxy_auth),
			"CURLOPT_PROXYUSERPWD");
	}

	/* authentication */
	if (strcmp(config.user_auth, "")) {
		handle_curl_option_return_code(
			curl_easy_setopt(result.curl_state.curl, CURLOPT_USERPWD, config.user_auth),
			"CURLOPT_USERPWD");
	}
	/* TODO: parameter auth method, bitfield of following methods:
	 * CURLAUTH_BASIC (default)
	 * CURLAUTH_DIGEST
	 * CURLAUTH_DIGEST_IE
	 * CURLAUTH_NEGOTIATE
	 * CURLAUTH_NTLM
	 * CURLAUTH_NTLM_WB
	 *
	 * convenience tokens for typical sets of methods:
	 * CURLAUTH_ANYSAFE: most secure, without BASIC
	 * or CURLAUTH_ANY: most secure, even BASIC if necessary
	 *
	 * handle_curl_option_return_code (curl_easy_setopt( curl, CURLOPT_HTTPAUTH,
	 * (long)CURLAUTH_DIGEST ), "CURLOPT_HTTPAUTH");
	 */

	/* handle redirections */
	if (on_redirect_dependent) {
		if (follow_method == FOLLOW_LIBCURL) {
			handle_curl_option_return_code(
				curl_easy_setopt(result.curl_state.curl, CURLOPT_FOLLOWLOCATION, 1L),
				"CURLOPT_FOLLOWLOCATION");

			/* default -1 is infinite, not good, could lead to zombie plugins!
			   Setting it to one bigger than maximal limit to handle errors nicely below
			 */
			handle_curl_option_return_code(
				curl_easy_setopt(result.curl_state.curl, CURLOPT_MAXREDIRS, max_depth + 1),
				"CURLOPT_MAXREDIRS");

			/* for now allow only http and https (we are a http(s) check plugin in the end) */
#if LIBCURL_VERSION_NUM >= MAKE_LIBCURL_VERSION(7, 85, 0)
			handle_curl_option_return_code(
				curl_easy_setopt(result.curl_state.curl, CURLOPT_REDIR_PROTOCOLS_STR, "http,https"),
				"CURLOPT_REDIR_PROTOCOLS_STR");
#elif LIBCURL_VERSION_NUM >= MAKE_LIBCURL_VERSION(7, 19, 4)
			handle_curl_option_return_code(curl_easy_setopt(result.curl_state.curl,
															CURLOPT_REDIR_PROTOCOLS,
															CURLPROTO_HTTP | CURLPROTO_HTTPS),
										   "CURLOPT_REDIRECT_PROTOCOLS");
#endif

			/* TODO: handle the following aspects of redirection, make them
			 * command line options too later:
			  CURLOPT_POSTREDIR: method switch
			  CURLINFO_REDIRECT_URL: custom redirect option
			  CURLOPT_REDIRECT_PROTOCOLS: allow people to step outside safe protocols
			  CURLINFO_REDIRECT_COUNT: get the number of redirects, print it, maybe a range
			 option here is nice like for expected page size?
			*/
		} else {
			/* old style redirection*/
		}
	}
	/* no-body */
	if (working_state.no_body) {
		handle_curl_option_return_code(curl_easy_setopt(result.curl_state.curl, CURLOPT_NOBODY, 1L),
									   "CURLOPT_NOBODY");
	}

	/* IPv4 or IPv6 forced DNS resolution */
	if (config.sin_family == AF_UNSPEC) {
		handle_curl_option_return_code(
			curl_easy_setopt(result.curl_state.curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_WHATEVER),
			"CURLOPT_IPRESOLVE(CURL_IPRESOLVE_WHATEVER)");
	} else if (config.sin_family == AF_INET) {
		handle_curl_option_return_code(
			curl_easy_setopt(result.curl_state.curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4),
			"CURLOPT_IPRESOLVE(CURL_IPRESOLVE_V4)");
	}
#if defined(LIBCURL_FEATURE_IPV6)
	else if (config.sin_family == AF_INET6) {
		handle_curl_option_return_code(
			curl_easy_setopt(result.curl_state.curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V6),
			"CURLOPT_IPRESOLVE(CURL_IPRESOLVE_V6)");
	}
#endif

	/* either send http POST data (any data, not only POST)*/
	if (!strcmp(working_state.http_method, "POST") || !strcmp(working_state.http_method, "PUT")) {
		/* set content of payload for POST and PUT */
		if (config.http_content_type) {
			snprintf(http_header, DEFAULT_BUFFER_SIZE, "Content-Type: %s",
					 config.http_content_type);
			result.curl_state.header_list =
				curl_slist_append(result.curl_state.header_list, http_header);
		}
		/* NULL indicates "HTTP Continue" in libcurl, provide an empty string
		 * in case of no POST/PUT data */
		if (!working_state.http_post_data) {
			working_state.http_post_data = "";
		}

		if (!strcmp(working_state.http_method, "POST")) {
			/* POST method, set payload with CURLOPT_POSTFIELDS */
			handle_curl_option_return_code(curl_easy_setopt(result.curl_state.curl,
															CURLOPT_POSTFIELDS,
															working_state.http_post_data),
										   "CURLOPT_POSTFIELDS");
		} else if (!strcmp(working_state.http_method, "PUT")) {
			handle_curl_option_return_code(
				curl_easy_setopt(result.curl_state.curl, CURLOPT_READFUNCTION,
								 (curl_read_callback)curlhelp_buffer_read_callback),
				"CURLOPT_READFUNCTION");
			if (curlhelp_initreadbuffer(&result.curl_state.put_buf, working_state.http_post_data,
										strlen(working_state.http_post_data)) < 0) {
				die(STATE_UNKNOWN,
					"HTTP CRITICAL - out of memory allocating read buffer for PUT\n");
			}
			result.curl_state.put_buf_initialized = true;
			handle_curl_option_return_code(curl_easy_setopt(result.curl_state.curl,
															CURLOPT_READDATA,
															(void *)result.curl_state.put_buf),
										   "CURLOPT_READDATA");
			handle_curl_option_return_code(
				curl_easy_setopt(result.curl_state.curl, CURLOPT_INFILESIZE,
								 (curl_off_t)strlen(working_state.http_post_data)),
				"CURLOPT_INFILESIZE");
		}
	}

	/* cookie handling */
	if (config.cookie_jar_file != NULL) {
		/* enable reading cookies from a file, and if the filename is an empty string, only
		 * enable the curl cookie engine */
		handle_curl_option_return_code(
			curl_easy_setopt(result.curl_state.curl, CURLOPT_COOKIEFILE, config.cookie_jar_file),
			"CURLOPT_COOKIEFILE");
		/* now enable saving cookies to a file, but only if the filename is not an empty string,
		 * since writing it would fail */
		if (*config.cookie_jar_file) {
			handle_curl_option_return_code(
				curl_easy_setopt(result.curl_state.curl, CURLOPT_COOKIEJAR, config.cookie_jar_file),
				"CURLOPT_COOKIEJAR");
		}
	}

	result.working_state = working_state;

	return result;
}

void handle_curl_option_return_code(CURLcode res, const char *option) {
	if (res != CURLE_OK) {
		die(STATE_CRITICAL, _("Error while setting cURL option '%s': cURL returned %d - %s\n"),
			option, res, curl_easy_strerror(res));
	}
}

char *get_header_value(const struct phr_header *headers, const size_t nof_headers,
					   const char *header) {
	for (size_t i = 0; i < nof_headers; i++) {
		if (headers[i].name != NULL &&
			strncasecmp(header, headers[i].name, max(headers[i].name_len, 4)) == 0) {
			return strndup(headers[i].value, headers[i].value_len);
		}
	}
	return NULL;
}

check_curl_working_state check_curl_working_state_init(void) {
	check_curl_working_state result = {
		.server_address = NULL,
		.server_url = DEFAULT_SERVER_URL,
		.host_name = NULL,
		.http_method = NULL,
		.http_post_data = NULL,
		.virtualPort = 0,
		.serverPort = HTTP_PORT,
		.use_ssl = false,
		.no_body = false,
		.curlopt_proxy = NULL,
		.curlopt_noproxy = NULL,
	};
	return result;
}

check_curl_config check_curl_config_init(void) {
	check_curl_config tmp = {
		.initial_config = check_curl_working_state_init(),

		.curl_config =
			{
				.automatic_decompression = false,
				.socket_timeout = DEFAULT_SOCKET_TIMEOUT,
				.haproxy_protocol = false,
				.sin_family = AF_UNSPEC,
				.curl_http_version = CURL_HTTP_VERSION_NONE,
				.http_opt_headers = NULL,
				.http_opt_headers_count = 0,
				.ssl_version = CURL_SSLVERSION_DEFAULT,
				.client_cert = NULL,
				.client_privkey = NULL,
				.ca_cert = NULL,
				.verify_peer_and_host = false,
				.user_agent = {'\0'},
				.proxy = "",
				.no_proxy = "",
				.proxy_auth = "",
				.user_auth = "",
				.http_content_type = NULL,
				.cookie_jar_file = NULL,
			},
		.max_depth = DEFAULT_MAX_REDIRS,
		.followmethod = FOLLOW_HTTP_CURL,
		.followsticky = STICKY_NONE,

		.maximum_age = -1,
		.regexp = {},
		.compiled_regex = {},
		.state_regex = STATE_CRITICAL,
		.invert_regex = false,
		.check_cert = false,
		.continue_after_check_cert = false,
		.days_till_exp_warn = 0,
		.days_till_exp_crit = 0,
		.thlds = mp_thresholds_init(),
		.page_length_limits = mp_range_init(),
		.page_length_limits_is_set = false,
		.server_expect =
			{
				.string = HTTP_EXPECT,
				.is_present = false,
			},
		.string_expect = "",
		.header_expect = "",
		.on_redirect_result_state = STATE_OK,
		.on_redirect_dependent = false,
		.on_timeout_result_state = STATE_CRITICAL,

		.show_extended_perfdata = false,
		.show_body = false,

		.output_format_is_set = false,
	};

	snprintf(tmp.curl_config.user_agent, DEFAULT_BUFFER_SIZE, "%s/v%s (monitoring-plugins %s, %s)",
			 "check_curl", NP_VERSION, VERSION, curl_version());

	return tmp;
}

/* TODO: is there a better way in libcurl to check for the SSL library? */
curlhelp_ssl_library curlhelp_get_ssl_library(void) {
	curlhelp_ssl_library ssl_library = CURLHELP_SSL_LIBRARY_UNKNOWN;

	curl_version_info_data *version_data = curl_version_info(CURLVERSION_NOW);
	if (version_data == NULL) {
		return CURLHELP_SSL_LIBRARY_UNKNOWN;
	}

	char *ssl_version = strdup(version_data->ssl_version);
	if (ssl_version == NULL) {
		return CURLHELP_SSL_LIBRARY_UNKNOWN;
	}

	char *library = strtok(ssl_version, "/");
	if (library == NULL) {
		return CURLHELP_SSL_LIBRARY_UNKNOWN;
	}

	if (strcmp(library, "OpenSSL") == 0) {
		ssl_library = CURLHELP_SSL_LIBRARY_OPENSSL;
	} else if (strcmp(library, "LibreSSL") == 0) {
		ssl_library = CURLHELP_SSL_LIBRARY_LIBRESSL;
	} else if (strcmp(library, "GnuTLS") == 0) {
		ssl_library = CURLHELP_SSL_LIBRARY_GNUTLS;
	} else if (strcmp(library, "NSS") == 0) {
		ssl_library = CURLHELP_SSL_LIBRARY_NSS;
	}

	if (verbose >= 2) {
		printf("* SSL library string is : %s %s (%d)\n", version_data->ssl_version, library,
			   ssl_library);
	}

	free(ssl_version);

	return ssl_library;
}

const char *curlhelp_get_ssl_library_string(const curlhelp_ssl_library ssl_library) {
	switch (ssl_library) {
	case CURLHELP_SSL_LIBRARY_OPENSSL:
		return "OpenSSL";
	case CURLHELP_SSL_LIBRARY_LIBRESSL:
		return "LibreSSL";
	case CURLHELP_SSL_LIBRARY_GNUTLS:
		return "GnuTLS";
	case CURLHELP_SSL_LIBRARY_NSS:
		return "NSS";
	case CURLHELP_SSL_LIBRARY_UNKNOWN:
	default:
		return "unknown";
	}
}

size_t get_content_length(const curlhelp_write_curlbuf *header_buf,
						  const curlhelp_write_curlbuf *body_buf) {
	struct phr_header headers[255];
	size_t nof_headers = 255;
	size_t msglen;
	curlhelp_statusline status_line;
	int res = phr_parse_response(header_buf->buf, header_buf->buflen, &status_line.http_major,
								 &status_line.http_minor, &status_line.http_code, &status_line.msg,
								 &msglen, headers, &nof_headers, 0);

	if (res == -1) {
		die(STATE_UNKNOWN, _("HTTP UNKNOWN - Failed to parse Response\n"));
	}

	char *content_length_s = get_header_value(headers, nof_headers, "content-length");
	if (!content_length_s) {
		return header_buf->buflen + body_buf->buflen;
	}

	content_length_s += strspn(content_length_s, " \t");
	size_t content_length = atoi(content_length_s);
	if (content_length != body_buf->buflen) {
		/* TODO: should we warn if the actual and the reported body length don't match? */
	}

	if (content_length_s) {
		free(content_length_s);
	}

	return header_buf->buflen + body_buf->buflen;
}

mp_subcheck check_document_dates(const curlhelp_write_curlbuf *header_buf, const int maximum_age) {
	struct phr_header headers[255];
	size_t nof_headers = 255;
	curlhelp_statusline status_line;
	size_t msglen;
	int res = phr_parse_response(header_buf->buf, header_buf->buflen, &status_line.http_major,
								 &status_line.http_minor, &status_line.http_code, &status_line.msg,
								 &msglen, headers, &nof_headers, 0);

	if (res == -1) {
		die(STATE_UNKNOWN, _("HTTP UNKNOWN - Failed to parse Response\n"));
	}

	char *server_date = get_header_value(headers, nof_headers, "date");
	char *document_date = get_header_value(headers, nof_headers, "last-modified");

	mp_subcheck sc_document_dates = mp_subcheck_init();
	if (!server_date || !*server_date) {
		xasprintf(&sc_document_dates.output, _("Server date unknown"));
		sc_document_dates = mp_set_subcheck_state(sc_document_dates, STATE_UNKNOWN);
	} else if (!document_date || !*document_date) {
		xasprintf(&sc_document_dates.output, _("Document modification date unknown, "));
		sc_document_dates = mp_set_subcheck_state(sc_document_dates, STATE_CRITICAL);
	} else {
		time_t srv_data = curl_getdate(server_date, NULL);
		time_t doc_data = curl_getdate(document_date, NULL);

		if (verbose >= 2) {
			printf("* server date: '%s' (%d), doc_date: '%s' (%d)\n", server_date, (int)srv_data,
				   document_date, (int)doc_data);
		}

		if (srv_data <= 0) {
			xasprintf(&sc_document_dates.output, _("Server date \"%100s\" unparsable"),
					  server_date);
			sc_document_dates = mp_set_subcheck_state(sc_document_dates, STATE_CRITICAL);
		} else if (doc_data <= 0) {

			xasprintf(&sc_document_dates.output, _("Document date \"%100s\" unparsable"),
					  document_date);
			sc_document_dates = mp_set_subcheck_state(sc_document_dates, STATE_CRITICAL);
		} else if (doc_data > srv_data + 30) {

			xasprintf(&sc_document_dates.output, _("Document is %d seconds in the future"),
					  (int)doc_data - (int)srv_data);

			sc_document_dates = mp_set_subcheck_state(sc_document_dates, STATE_CRITICAL);
		} else if (doc_data < srv_data - maximum_age) {
			time_t last_modified = (srv_data - doc_data);
			if (last_modified > (60 * 60 * 24 * 2)) { // two days hardcoded?
				xasprintf(&sc_document_dates.output, _("Last modified %.1f days ago"),
						  ((float)last_modified) / (60 * 60 * 24));
				sc_document_dates = mp_set_subcheck_state(sc_document_dates, STATE_CRITICAL);
			} else {
				xasprintf(&sc_document_dates.output, _("Last modified %lld:%02d:%02d ago"),
						  (long long)last_modified / (60 * 60), (int)(last_modified / 60) % 60,
						  (int)last_modified % 60);
				sc_document_dates = mp_set_subcheck_state(sc_document_dates, STATE_CRITICAL);
			}
		} else {
			// TODO is this the OK case?
			time_t last_modified = (srv_data - doc_data);
			xasprintf(&sc_document_dates.output, _("Last modified %lld:%02d:%02d ago"),
					  (long long)last_modified / (60 * 60), (int)(last_modified / 60) % 60,
					  (int)last_modified % 60);
			sc_document_dates = mp_set_subcheck_state(sc_document_dates, STATE_OK);
		}
	}

	if (server_date) {
		free(server_date);
	}
	if (document_date) {
		free(document_date);
	}

	return sc_document_dates;
}

void curlhelp_free_statusline(curlhelp_statusline *status_line) { free(status_line->first_line); }

int curlhelp_parse_statusline(const char *buf, curlhelp_statusline *status_line) {
	/* find last start of a new header */
	const char *start = strrstr2(buf, "\r\nHTTP/");
	if (start != NULL) {
		start += 2;
		buf = start;
	}

	// Accept either LF or CRLF as end of line for the status line
	// CRLF is the standard (RFC9112), but it is recommended to accept both
	size_t length_of_first_line = strcspn(buf, "\r\n");
	const char *first_line_end = &buf[length_of_first_line];
	if (first_line_end == NULL) {
		return -1;
	}

	size_t first_line_len = (size_t)(first_line_end - buf);
	status_line->first_line = (char *)calloc(first_line_len + 1, sizeof(char));
	if (status_line->first_line == NULL) {
		return -1;
	}

	memcpy(status_line->first_line, buf, first_line_len);
	status_line->first_line[first_line_len] = '\0';
	char *first_line_buf = strdup(status_line->first_line);

	/* protocol and version: "HTTP/x.x" SP or "HTTP/2" SP */
	char *temp_string = strtok(first_line_buf, "/");
	if (temp_string == NULL) {
		if (verbose > 1) {
			printf("%s: no / found\n", __func__);
		}
		free(first_line_buf);
		return -1;
	}

	if (strcmp(temp_string, "HTTP") != 0) {
		if (verbose > 1) {
			printf("%s: string 'HTTP' not found\n", __func__);
		}
		free(first_line_buf);
		return -1;
	}

	// try to find a space in the remaining string?
	// the space after HTTP/1.1 probably
	temp_string = strtok(NULL, " ");
	if (temp_string == NULL) {
		if (verbose > 1) {
			printf("%s: no space after protocol definition\n", __func__);
		}
		free(first_line_buf);
		return -1;
	}

	char *temp_string_2;
	if (strchr(temp_string, '.') != NULL) {
		/* HTTP 1.x case */
		strtok(temp_string, ".");
		status_line->http_major = (int)strtol(temp_string, &temp_string_2, 10);
		if (*temp_string_2 != '\0') {
			free(first_line_buf);
			return -1;
		}
		strtok(NULL, " ");
		status_line->http_minor = (int)strtol(temp_string, &temp_string_2, 10);
		if (*temp_string_2 != '\0') {
			free(first_line_buf);
			return -1;
		}
		temp_string += 4; /* 1.x SP */
	} else {
		/* HTTP 2 case */
		status_line->http_major = (int)strtol(temp_string, &temp_string_2, 10);
		status_line->http_minor = 0;
		temp_string += 2; /* 2 SP */
	}

	/* status code: "404" or "404.1", then SP */
	temp_string = strtok(temp_string, " ");
	if (temp_string == NULL) {
		free(first_line_buf);
		return -1;
	}
	if (strchr(temp_string, '.') != NULL) {
		char *ppp;
		ppp = strtok(temp_string, ".");
		status_line->http_code = (int)strtol(ppp, &temp_string_2, 10);
		if (*temp_string_2 != '\0') {
			free(first_line_buf);
			return -1;
		}
		ppp = strtok(NULL, "");
		status_line->http_subcode = (int)strtol(ppp, &temp_string_2, 10);
		if (*temp_string_2 != '\0') {
			free(first_line_buf);
			return -1;
		}
		temp_string += 6; /* 400.1 SP */
	} else {
		status_line->http_code = (int)strtol(temp_string, &temp_string_2, 10);
		status_line->http_subcode = -1;
		if (*temp_string_2 != '\0') {
			free(first_line_buf);
			return -1;
		}
		temp_string += 4; /* 400 SP */
	}

	/* Human readable message: "Not Found" CRLF */

	temp_string = strtok(temp_string, "");
	if (temp_string == NULL) {
		status_line->msg = "";
		return 0;
	}
	status_line->msg = status_line->first_line + (temp_string - first_line_buf);
	free(first_line_buf);

	return 0;
}

/* TODO: where to put this, it's actually part of sstrings2 (logically)?
 */
const char *strrstr2(const char *haystack, const char *needle) {
	if (haystack == NULL || needle == NULL) {
		return NULL;
	}

	if (haystack[0] == '\0' || needle[0] == '\0') {
		return NULL;
	}

	int counter = 0;
	const char *prev_pos = NULL;
	const char *pos = haystack;
	size_t len = strlen(needle);
	for (;;) {
		pos = strstr(pos, needle);
		if (pos == NULL) {
			if (counter == 0) {
				return NULL;
			}
			return prev_pos;
		}
		counter++;
		prev_pos = pos;
		pos += len;
		if (*pos == '\0') {
			return prev_pos;
		}
	}
}

void curlhelp_freereadbuffer(curlhelp_read_curlbuf *buf) {
	free(buf->buf);
	buf->buf = NULL;
}

void curlhelp_freewritebuffer(curlhelp_write_curlbuf *buf) {
	free(buf->buf);
	buf->buf = NULL;
}

int curlhelp_initreadbuffer(curlhelp_read_curlbuf **buf, const char *data, size_t datalen) {
	if ((*buf = calloc(1, sizeof(curlhelp_read_curlbuf))) == NULL) {
		return 1;
	}

	(*buf)->buflen = datalen;
	(*buf)->buf = (char *)calloc((*buf)->buflen, sizeof(char));
	if ((*buf)->buf == NULL) {
		return -1;
	}
	memcpy((*buf)->buf, data, datalen);
	(*buf)->pos = 0;
	return 0;
}

size_t curlhelp_buffer_read_callback(void *buffer, size_t size, size_t nmemb, void *stream) {
	curlhelp_read_curlbuf *buf = (curlhelp_read_curlbuf *)stream;

	size_t minimalSize = min(nmemb * size, buf->buflen - buf->pos);

	memcpy(buffer, buf->buf + buf->pos, minimalSize);
	buf->pos += minimalSize;

	return minimalSize;
}

int curlhelp_initwritebuffer(curlhelp_write_curlbuf **buf) {
	if ((*buf = calloc(1, sizeof(curlhelp_write_curlbuf))) == NULL) {
		return 1;
	}
	(*buf)->bufsize = DEFAULT_BUFFER_SIZE * sizeof(char);
	(*buf)->buflen = 0;
	(*buf)->buf = (char *)calloc((*buf)->bufsize, sizeof(char));
	if ((*buf)->buf == NULL) {
		return -1;
	}
	return 0;
}

size_t curlhelp_buffer_write_callback(void *buffer, size_t size, size_t nmemb, void *stream) {
	curlhelp_write_curlbuf *buf = (curlhelp_write_curlbuf *)stream;

	while (buf->bufsize < buf->buflen + size * nmemb + 1) {
		buf->bufsize = buf->bufsize * 2;
		buf->buf = (char *)realloc(buf->buf, buf->bufsize);
		if (buf->buf == NULL) {
			fprintf(stderr, "malloc failed (%d) %s\n", errno, strerror(errno));
			return 0;
		}
	}

	memcpy(buf->buf + buf->buflen, buffer, size * nmemb);
	buf->buflen += size * nmemb;
	buf->buf[buf->buflen] = '\0';

	return size * nmemb;
}

void cleanup(check_curl_global_state global_state) {
	if (global_state.status_line_initialized) {
		curlhelp_free_statusline(global_state.status_line);
	}
	global_state.status_line_initialized = false;

	if (global_state.curl_easy_initialized) {
		curl_easy_cleanup(global_state.curl);
	}
	global_state.curl_easy_initialized = false;

	if (global_state.curl_global_initialized) {
		curl_global_cleanup();
	}
	global_state.curl_global_initialized = false;

	if (global_state.body_buf_initialized) {
		curlhelp_freewritebuffer(global_state.body_buf);
	}
	global_state.body_buf_initialized = false;

	if (global_state.header_buf_initialized) {
		curlhelp_freewritebuffer(global_state.header_buf);
	}
	global_state.header_buf_initialized = false;

	if (global_state.put_buf_initialized) {
		curlhelp_freereadbuffer(global_state.put_buf);
	}
	global_state.put_buf_initialized = false;

	if (global_state.header_list) {
		curl_slist_free_all(global_state.header_list);
	}

	if (global_state.host) {
		curl_slist_free_all(global_state.host);
	}
}

int lookup_host(const char *host, char *buf, size_t buflen, sa_family_t addr_family) {
	struct addrinfo hints = {
		.ai_family = addr_family,
		.ai_socktype = SOCK_STREAM,
		.ai_flags = AI_CANONNAME,
	};

	struct addrinfo *result;
	int errcode = getaddrinfo(host, NULL, &hints, &result);
	if (errcode != 0) {
		return errcode;
	}

	strcpy(buf, "");
	struct addrinfo *res = result;

	size_t buflen_remaining = buflen - 1;
	size_t addrstr_len;
	char addrstr[100];
	void *ptr = {0};
	while (res) {
		switch (res->ai_family) {
		case AF_INET:
			ptr = &((struct sockaddr_in *)res->ai_addr)->sin_addr;
			break;
		case AF_INET6:
			ptr = &((struct sockaddr_in6 *)res->ai_addr)->sin6_addr;
			break;
		}

		inet_ntop(res->ai_family, ptr, addrstr, 100);
		if (verbose >= 1) {
			printf("* getaddrinfo IPv%d address: %s\n", res->ai_family == PF_INET6 ? 6 : 4,
				   addrstr);
		}

		// Append all IPs to buf as a comma-separated string
		addrstr_len = strlen(addrstr);
		if (buflen_remaining > addrstr_len + 1) {
			if (buf[0] != '\0') {
				strncat(buf, ",", buflen_remaining);
				buflen_remaining -= 1;
			}
			strncat(buf, addrstr, buflen_remaining);
			buflen_remaining -= addrstr_len;
		}

		res = res->ai_next;
	}

	freeaddrinfo(result);

	return 0;
}

/* Checks if the server 'reply' is one of the expected 'statuscodes' */
bool expected_statuscode(const char *reply, const char *statuscodes) {
	char *expected;

	if ((expected = strdup(statuscodes)) == NULL) {
		die(STATE_UNKNOWN, _("HTTP UNKNOWN - Memory allocation error\n"));
	}

	bool result = false;
	for (char *code = strtok(expected, ","); code != NULL; code = strtok(NULL, ",")) {
		if (strstr(reply, code) != NULL) {
			result = true;
			break;
		}
	}

	free(expected);
	return result;
}

/* returns a string "HTTP/1.x" or "HTTP/2" */
char *string_statuscode(int major, int minor) {
	static char buf[10];

	switch (major) {
	case 1:
		snprintf(buf, sizeof(buf), "HTTP/%d.%d", major, minor);
		break;
	case 2:
	case 3:
		snprintf(buf, sizeof(buf), "HTTP/%d", major);
		break;
	default:
		/* assuming here HTTP/N with N>=4 */
		snprintf(buf, sizeof(buf), "HTTP/%d", major);
		break;
	}

	return buf;
}

/* check whether a file exists */
void test_file(char *path) {
	if (access(path, R_OK) == 0) {
		return;
	}
	usage2(_("file does not exist or is not readable"), path);
}

mp_subcheck mp_net_ssl_check_certificate(X509 *certificate, int days_till_exp_warn,
										 int days_till_exp_crit);

mp_subcheck check_curl_certificate_checks(CURL *curl, X509 *cert, int warn_days_till_exp,
										  int crit_days_till_exp) {
	mp_subcheck sc_cert_result = mp_subcheck_init();
	sc_cert_result = mp_set_subcheck_default_state(sc_cert_result, STATE_OK);

#ifdef LIBCURL_FEATURE_SSL
	if (is_openssl_callback) {
#	ifdef MOPL_USE_OPENSSL
		/* check certificate with OpenSSL functions, curl has been built against OpenSSL
		 * and we actually have OpenSSL in the monitoring tools
		 */
		return mp_net_ssl_check_certificate(cert, warn_days_till_exp, crit_days_till_exp);
#	else  /* MOPL_USE_OPENSSL */
		xasprintf(&result.output, "HTTP CRITICAL - Cannot retrieve certificates - OpenSSL "
								  "callback used and not linked against OpenSSL\n");
		mp_set_subcheck_state(result, STATE_CRITICAL);
#	endif /* MOPL_USE_OPENSSL */
	} else {
		struct curl_slist *slist;

		cert_ptr_union cert_ptr = {0};
		cert_ptr.to_info = NULL;
		CURLcode res = curl_easy_getinfo(curl, CURLINFO_CERTINFO, &cert_ptr.to_certinfo);
		if (!res && cert_ptr.to_info) {
#	ifdef MOPL_USE_OPENSSL
			/* We have no OpenSSL in libcurl, but we can use OpenSSL for X509 cert
			 * parsing We only check the first certificate and assume it's the one of
			 * the server
			 */
			char *raw_cert = NULL;
			bool got_first_cert = false;
			for (int i = 0; i < cert_ptr.to_certinfo->num_of_certs; i++) {
				if (got_first_cert) {
					break;
				}

				for (slist = cert_ptr.to_certinfo->certinfo[i]; slist; slist = slist->next) {
					if (verbose >= 2) {
						printf("%d ** %s\n", i, slist->data);
					}
					if (strncmp(slist->data, "Cert:", 5) == 0) {
						raw_cert = &slist->data[5];
						got_first_cert = true;
						break;
					}
				}
			}

			if (!raw_cert) {

				xasprintf(&sc_cert_result.output,
						  _("Cannot retrieve certificates from CERTINFO information - "
							"certificate data was empty"));
				sc_cert_result = mp_set_subcheck_state(sc_cert_result, STATE_CRITICAL);
				return sc_cert_result;
			}

			BIO *cert_BIO = BIO_new(BIO_s_mem());
			BIO_write(cert_BIO, raw_cert, (int)strlen(raw_cert));

			cert = PEM_read_bio_X509(cert_BIO, NULL, NULL, NULL);
			if (!cert) {
				xasprintf(&sc_cert_result.output,
						  _("Cannot read certificate from CERTINFO information - BIO error"));
				sc_cert_result = mp_set_subcheck_state(sc_cert_result, STATE_CRITICAL);
				return sc_cert_result;
			}

			BIO_free(cert_BIO);
			return mp_net_ssl_check_certificate(cert, warn_days_till_exp, crit_days_till_exp);
#	else  /* MOPL_USE_OPENSSL */
			/* We assume we don't have OpenSSL and np_net_ssl_check_certificate at our
			 * disposal, so we use the libcurl CURLINFO data
			 */
			return net_noopenssl_check_certificate(&cert_ptr, days_till_exp_warn,
												   days_till_exp_crit);
#	endif /* MOPL_USE_OPENSSL */
		} else {
			xasprintf(&sc_cert_result.output,
					  _("Cannot retrieve certificates - cURL returned %d - %s"), res,
					  curl_easy_strerror(res));
			mp_set_subcheck_state(sc_cert_result, STATE_CRITICAL);
		}
	}
#endif /* LIBCURL_FEATURE_SSL */

	return sc_cert_result;
}

char *fmt_url(check_curl_working_state workingState) {
	char *url = calloc(DEFAULT_BUFFER_SIZE, sizeof(char));
	if (url == NULL) {
		die(STATE_UNKNOWN, "memory allocation failed");
	}

	snprintf(url, DEFAULT_BUFFER_SIZE, "%s://%s:%d%s", workingState.use_ssl ? "https" : "http",
			 (workingState.use_ssl & (workingState.host_name != NULL))
				 ? workingState.host_name
				 : workingState.server_address,
			 workingState.serverPort, workingState.server_url);

	return url;
}

bool hostname_gets_resolved_locally(const check_curl_working_state working_state) {
	char *host_name_display = "NULL";
	unsigned long host_name_len = 0;
	if (working_state.host_name) {
		host_name_len = strlen(working_state.host_name);
		host_name_display = working_state.host_name;
	}

	/* IPv4 or IPv6 version of the address, this variable saves both */
	char *server_address_clean = strdup(working_state.server_address);
	/* server address might be a full length ipv6 address encapsulated in square brackets */
	if ((strnlen(working_state.server_address, MAX_IPV4_HOSTLENGTH) > 2) &&
		(working_state.server_address[0] == '[') &&
		(working_state.server_address[strlen(working_state.server_address) - 1] == ']')) {
		free(server_address_clean);
		server_address_clean =
			strndup(working_state.server_address + 1, strlen(working_state.server_address) - 2);
	}

	/* check curlopt_noproxy option before trying to understand this function */
	/* https://curl.se/libcurl/c/CURLOPT_NOPROXY.html */

	/* curlopt_noproxy is specified as a comma separated list of
	direct IPv4 or IPv6 addresses e.g 130.133.8.40, 2001:4860:4802:32::a ,
	IPv4 or IPv6 CIDR regions e.g 10.241.0.0/16 , abcd:ef01:2345::/48 ,
	direct hostnames e.g example.com, google.de */

	if (working_state.curlopt_noproxy != NULL) {
		char *curlopt_noproxy_copy = strdup(working_state.curlopt_noproxy);
		char *noproxy_item = strtok(curlopt_noproxy_copy, ",");
		while (noproxy_item != NULL) {
			unsigned long noproxy_item_len = strlen(noproxy_item);

			/* According to the CURLOPT_NOPROXY documentation: */
			/* https://curl.se/libcurl/c/CURLOPT_NOPROXY.html */
			/* The only wildcard available is a single * character, which matches all hosts, and
			 * effectively disables the proxy. */
			if (strlen(noproxy_item) == 1 && noproxy_item[0] == '*') {
				if (verbose >= 1) {
					printf(
						"* noproxy includes '*' which disables proxy for all host name including : "
						"%s / server address including : %s\n",
						host_name_display, server_address_clean);
				}
				free(curlopt_noproxy_copy);
				free(server_address_clean);
				return true;
			}

			/* direct comparison with the server_address */
			if (server_address_clean != NULL &&
				strlen(server_address_clean) == strlen(noproxy_item) &&
				strcmp(server_address_clean, noproxy_item) == 0) {
				if (verbose >= 1) {
					printf("* server_address is in the no_proxy list: %s\n", noproxy_item);
				}
				free(curlopt_noproxy_copy);
				free(server_address_clean);
				return true;
			}

			/* direct comparison with the host_name */
			if (working_state.host_name != NULL && host_name_len == noproxy_item_len &&
				strcmp(working_state.host_name, noproxy_item) == 0) {
				if (verbose >= 1) {
					printf("* host_name is in the no_proxy list: %s\n", noproxy_item);
				}
				free(curlopt_noproxy_copy);
				free(server_address_clean);
				return true;
			}

			/* check if hostname is a subdomain of the item, e.g www.example.com when token is
			 * example.com */
			/* subdomain1.acme.com will not will use a proxy if you only specify 'acme' in the
			 * noproxy */
			/* check if noproxy_item is a suffix */
			/* check if the character just before the suffix is '.' */
			if (working_state.host_name != NULL && host_name_len > noproxy_item_len) {
				unsigned long suffix_start_idx = host_name_len - noproxy_item_len;
				if (strcmp(working_state.host_name + suffix_start_idx, noproxy_item) == 0 &&
					working_state.host_name[suffix_start_idx - 1] == '.') {
					if (verbose >= 1) {
						printf("* host_name: %s is a subdomain of the no_proxy list item: %s\n",
							   working_state.host_name, noproxy_item);
					}
					free(curlopt_noproxy_copy);
					free(server_address_clean);
					return true;
				}
			}

			// noproxy_item could be a CIDR IP range
			if (server_address_clean != NULL && strlen(server_address_clean)) {
				ip_addr_inside ip_addr_inside_cidr_ret =
					ip_addr_inside_cidr(noproxy_item, server_address_clean);

				if (ip_addr_inside_cidr_ret.error == NO_ERROR) {
					if (ip_addr_inside_cidr_ret.inside) {
						free(curlopt_noproxy_copy);
						free(server_address_clean);
						return true;
					} else {
						if (verbose >= 1) {
							printf("server address: %s is not inside IP CIDR: %s\n",
								   server_address_clean, noproxy_item);
						}
					}
				} else {
					if (verbose >= 1) {
						printf("could not fully determine if server address: %s is inside the IP "
							   "CIDR: %s\n",
							   server_address_clean, noproxy_item);
					}
				}
			}

			noproxy_item = strtok(NULL, ",");
		}

		free(curlopt_noproxy_copy);
	}

	if (working_state.curlopt_proxy != NULL) {
		// Libcurl documentation
		// Setting the proxy string to "" (an empty string) explicitly disables the use of a proxy,
		// even if there is an environment variable set for it.
		if (strlen(working_state.curlopt_proxy) == 0) {
			return true;
		}

		if (strncmp(working_state.curlopt_proxy, "http://", 7) == 0) {
			if (verbose >= 1) {
				printf(
					"* proxy scheme is http, proxy: %s resolves host: %s or server_address: %s\n",
					working_state.curlopt_proxy, host_name_display, server_address_clean);
			}
			free(server_address_clean);
			return false;
		}

		if (strncmp(working_state.curlopt_proxy, "https://", 8) == 0) {
			if (verbose >= 1) {
				printf(
					"* proxy scheme is https, proxy: %s resolves host: %s or server_address: %s\n",
					working_state.curlopt_proxy, host_name_display, server_address_clean);
			}
			free(server_address_clean);
			return false;
		}

		if (strncmp(working_state.curlopt_proxy, "socks4://", 9) == 0) {
			if (verbose >= 1) {
				printf("* proxy scheme is socks, proxy: %s does not resolve host: %s or "
					   "server_address: %s\n",
					   working_state.curlopt_proxy, host_name_display, server_address_clean);
			}
			free(server_address_clean);
			return true;
		}

		if (strncmp(working_state.curlopt_proxy, "socks4a://", 10) == 0) {
			if (verbose >= 1) {
				printf("* proxy scheme is socks4a, proxy: %s resolves host: %s or server_address: "
					   "%s\n",
					   working_state.curlopt_proxy, host_name_display, server_address_clean);
			}
			free(server_address_clean);
			return false;
		}

		if (strncmp(working_state.curlopt_proxy, "socks5://", 9) == 0) {
			if (verbose >= 1) {
				printf("* proxy scheme is socks5, proxy: %s does not resolve host: %s or "
					   "server_address: %s\n",
					   working_state.curlopt_proxy, host_name_display, server_address_clean);
			}
			free(server_address_clean);
			return true;
		}

		if (strncmp(working_state.curlopt_proxy, "socks5h://", 10) == 0) {
			if (verbose >= 1) {
				printf("* proxy scheme is socks5h, proxy: %s resolves host: %s or server_address: "
					   "%s\n",
					   working_state.curlopt_proxy, host_name_display, server_address_clean);
			}
			free(server_address_clean);
			return false;
		}

		// Libcurl documentation:
		// Without a scheme prefix, CURLOPT_PROXYTYPE can be used to specify which kind of proxy the
		// string identifies. We do not set this value Without a scheme, it is treated as an http
		// proxy

		if (verbose >= 1) {
			printf("* proxy scheme is unspecified, and therefore taken as http, proxy: %s resolves "
				   "host: %s or server_address: %s\n",
				   working_state.curlopt_proxy, host_name_display, server_address_clean);
		}

		return false;
	}

	if (verbose >= 1) {
		printf("* proxy is unknown/unavailable, no proxy is assumed for host: %s or "
			   "server_address: %s\n",
			   host_name_display, server_address_clean);
	}

	free(server_address_clean);
	return true;
}

ip_addr_inside ip_addr_inside_cidr(const char *cidr_region_or_ip_addr, const char *target_ip) {
	unsigned int slash_count = 0;
	unsigned int last_slash_idx = 0;
	for (size_t i = 0; i < strlen(cidr_region_or_ip_addr); i++) {
		if (cidr_region_or_ip_addr[i] == '/') {
			slash_count++;
			last_slash_idx = (unsigned int)i;
		}
	}

	char *cidr_ip_part = NULL;
	int prefix_length = 0;
	ip_addr_inside result = {
		.inside = false,
		.error = NO_ERROR,
	};

	if (slash_count == 0) {
		cidr_ip_part = strdup(cidr_region_or_ip_addr);
		if (!cidr_ip_part) {
			result.error = FAILED_STRDUP;
			return result;
		}
	} else if (slash_count == 1) {
		cidr_ip_part = strndup(cidr_region_or_ip_addr, last_slash_idx);
		if (!cidr_ip_part) {
			result.error = FAILED_STRDUP;
			return result;
		}

		errno = 0;
		long long tmp = strtoll(cidr_region_or_ip_addr + last_slash_idx + 1, NULL, 10);
		if (errno == ERANGE) {
			if (verbose >= 1) {
				printf("cidr_region_or_ip: %s , could not parse subnet length\n",
					   cidr_region_or_ip_addr);
			}
			free(cidr_ip_part);
			result.error = COULD_NOT_PARSE_SUBNET_LENGTH;
			return result;
		}
		prefix_length = (int)tmp;
	} else {
		if (verbose >= 1) {
			printf("cidr_region_or_ip: %s , has %u number of '/' characters, is not a valid "
				   "cidr_region or IP\n",
				   cidr_region_or_ip_addr, slash_count);
		}
		result.error = CIDR_REGION_INVALID;
		return result;
	}

	int cidr_addr_family, target_addr_family;
	if (strchr(cidr_ip_part, ':')) {
		cidr_addr_family = AF_INET6;
	} else {
		cidr_addr_family = AF_INET;
	}

	if (strchr(target_ip, ':')) {
		target_addr_family = AF_INET6;
	} else {
		target_addr_family = AF_INET;
	}

	if (cidr_addr_family != target_addr_family) {
		if (verbose >= 1) {
			printf("cidr address: %s and target ip address: %s have different address families\n",
				   cidr_ip_part, target_ip);
		}
		free(cidr_ip_part);
		result.inside = false;
		return result;
	}

	// If no prefix is given, treat the cidr as a single address (full-length prefix)
	if (slash_count == 0) {
		prefix_length = (cidr_addr_family == AF_INET) ? 32 : 128;
	}

	int max_bits = (cidr_addr_family == AF_INET) ? 32u : 128u;
	if (prefix_length < 0 || prefix_length > max_bits) {
		if (verbose >= 1) {
			printf("cidr_region_or_ip: %s has invalid prefix length: %u\n", cidr_region_or_ip_addr,
				   prefix_length);
		}
		free(cidr_ip_part);
		result.error = CIDR_REGION_INVALID_PREFIX;
		return result;
	}

	if (verbose >= 1) {
		printf("cidr_region_or_ip: %s , has prefix length: %u\n", cidr_region_or_ip_addr,
			   prefix_length);
	}

	int inet_pton_rc;
	uint8_t *cidr_bytes = NULL;
	uint8_t *target_bytes = NULL;
	uint8_t cidr_buf[16];
	uint8_t target_buf[16];

	if (cidr_addr_family == AF_INET) {
		struct in_addr cidr_ipv4;
		struct in_addr target_ipv4;
		inet_pton_rc = inet_pton(AF_INET, cidr_ip_part, &cidr_ipv4);
		if (inet_pton_rc != 1) {
			if (verbose >= 1) {
				printf("ip string: %s contains characters not valid for its address family: IPv4\n",
					   cidr_ip_part);
			}
			free(cidr_ip_part);
			result.error = IP_CONTAINS_INVALID_CHARACTERS;
			return result;
		}
		inet_pton_rc = inet_pton(AF_INET, target_ip, &target_ipv4);
		if (inet_pton_rc != 1) {
			if (verbose >= 1) {
				printf("ip string: %s contains characters not valid for its address family: IPv4\n",
					   target_ip);
			}
			free(cidr_ip_part);
			result.error = IP_CONTAINS_INVALID_CHARACTERS;
			return result;
		}
		// copy the addresses in network byte order to a buffer for comparison
		memcpy(cidr_buf, &cidr_ipv4.s_addr, 4);
		memcpy(target_buf, &target_ipv4.s_addr, 4);
		cidr_bytes = cidr_buf;
		target_bytes = target_buf;
	} else {
		struct in6_addr cidr_ipv6;
		struct in6_addr target_ipv6;
		inet_pton_rc = inet_pton(AF_INET6, cidr_ip_part, &cidr_ipv6);
		if (inet_pton_rc != 1) {
			if (verbose >= 1) {
				printf("ip string: %s contains characters not valid for its address family: IPv6\n",
					   cidr_ip_part);
			}
			free(cidr_ip_part);
			result.error = IP_CONTAINS_INVALID_CHARACTERS;
			return result;
		}
		inet_pton_rc = inet_pton(AF_INET6, target_ip, &target_ipv6);
		if (inet_pton_rc != 1) {
			if (verbose >= 1) {
				printf("ip string: %s contains characters not valid for its address family: IPv6\n",
					   target_ip);
			}
			free(cidr_ip_part);
			result.error = IP_CONTAINS_INVALID_CHARACTERS;
			return result;
		}
		memcpy(cidr_buf, &cidr_ipv6, 16);
		memcpy(target_buf, &target_ipv6, 16);
		cidr_bytes = cidr_buf;
		target_bytes = target_buf;
	}

	int prefix_bytes = prefix_length / 8;
	int prefix_bits = prefix_length % 8;

	if (prefix_bytes > 0) {
		if (memcmp(cidr_bytes, target_bytes, (size_t)prefix_bytes) != 0) {
			if (verbose >= 1) {
				printf("the first %d bytes of the cidr_region_or_ip: %s and target_ip: %s are "
					   "different\n",
					   prefix_bytes, cidr_ip_part, target_ip);
			}
			free(cidr_ip_part);
			result.inside = false;
			return result;
		}
	}

	if (prefix_bits != 0) {
		uint8_t cidr_oct = cidr_bytes[prefix_bytes];
		uint8_t target_oct = target_bytes[prefix_bytes];
		// the mask has first prefix_bits bits 1, the rest as 0
		uint8_t mask = (uint8_t)(0xFFu << (8 - prefix_bits));
		if ((cidr_oct & mask) != (target_oct & mask)) {
			if (verbose >= 1) {
				printf("looking at the last %d bits of the prefix, cidr_region_or_ip(%s) byte is: "
					   "%u and target_ip byte(%s) is: %u, applying bitmask: %02X returns different "
					   "results\n",
					   prefix_bits, cidr_ip_part, (unsigned)cidr_oct, target_ip,
					   (unsigned)target_oct, mask);
			}
			free(cidr_ip_part);
			result.inside = false;
			return result;
		}
	}

	free(cidr_ip_part);
	result.inside = true;
	return result;
}
