/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

/***************************************************************************
 * Copyright (C) 2017-2026 ZmartZone Holding BV
 * Copyright (C) 2013-2017 Ping Identity Corporation
 * All rights reserved.
 *
 * DISCLAIMER OF WARRANTIES:
 *
 * THE SOFTWARE PROVIDED HEREUNDER IS PROVIDED ON AN "AS IS" BASIS, WITHOUT
 * ANY WARRANTIES OR REPRESENTATIONS EXPRESS, IMPLIED OR STATUTORY; INCLUDING,
 * WITHOUT LIMITATION, WARRANTIES OF QUALITY, PERFORMANCE, NONINFRINGEMENT,
 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.  NOR ARE THERE ANY
 * WARRANTIES CREATED BY A COURSE OR DEALING, COURSE OF PERFORMANCE OR TRADE
 * USAGE.  FURTHERMORE, THERE ARE NO WARRANTIES THAT THE SOFTWARE WILL MEET
 * YOUR NEEDS OR BE FREE FROM ERRORS, OR THAT THE OPERATION OF THE SOFTWARE
 * WILL BE UNINTERRUPTED.  IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @Author: Hans Zandbelt - hans.zandbelt@openidc.com
 */

#include <stddef.h>
#ifndef WIN32
#include <unistd.h>
#endif

#include <apr_strings.h>

#include <curl/curl.h>
#include <openssl/opensslv.h>

#include "cfg/dir.h"
#include "const.h"
#include "http.h"
#include "http_int.h"
#include "metrics.h"
#include "proto/proto.h"
#include "util/util.h"
#include "util/util_cfg.h"

/* the reusable-handle pool lives at the bottom of this file */
static CURL *oidc_http_curl_acquire(void);
static void oidc_http_curl_release(CURL *curl, apr_byte_t reuse);

/*
 * URL-encode a string: percent-encode every byte outside of the RFC 3986 unreserved set,
 * matching curl_easy_escape which was used here before (but required a throwaway CURL handle
 * per call since handles must not be shared between threads)
 */
char *oidc_http_url_encode(const request_rec *r, const char *str) {
	static const char hex[] = "0123456789ABCDEF";
	char *rv = NULL;
	char *p = NULL;
	size_t len = 0;

	if (str == NULL)
		return "";

	len = _oidc_strlen(str);

	/* worst case each input byte expands to a 3-byte %XX sequence */
	rv = apr_palloc(r->pool, len * 3 + 1);
	p = rv;
	for (size_t i = 0; i < len; i++) {
		const unsigned char c = (unsigned char)str[i];
		if (((c >= 'A') && (c <= 'Z')) || ((c >= 'a') && (c <= 'z')) || ((c >= '0') && (c <= '9')) ||
		    (c == '-') || (c == '.') || (c == '_') || (c == '~')) {
			*p++ = (char)c;
		} else {
			*p++ = '%';
			*p++ = hex[c >> 4];
			*p++ = hex[c & 0x0f];
		}
	}
	*p = '\0';

	return rv;
}

static int oidc_http_url_decode_hex_digit(const char c) {
	if ((c >= '0') && (c <= '9'))
		return c - '0';
	if ((c >= 'A') && (c <= 'F'))
		return c - 'A' + 10;
	if ((c >= 'a') && (c <= 'f'))
		return c - 'a' + 10;
	return -1;
}

/*
 * URL-decode a string: form-decode "+" to space and percent-decode %XX sequences, copying
 * malformed/truncated %-sequences through literally, matching the curl_easy_unescape based
 * implementation that was used here before
 */
char *oidc_http_url_decode(const request_rec *r, const char *str) {
	char *rv = NULL;
	char *p = NULL;
	size_t i = 0;
	size_t len = 0;

	if (str == NULL)
		return "";

	len = _oidc_strlen(str);

	rv = apr_palloc(r->pool, len + 1);
	p = rv;
	while (i < len) {
		const char c = str[i];
		if (c == '+') {
			*p++ = ' ';
			i++;
		} else if (c == '%') {
			/* str[i + 1] is within bounds (at worst the NUL terminator); str[i + 2] is only
			 * read when str[i + 1] is a hex digit and thus not the terminator */
			const int hi = oidc_http_url_decode_hex_digit(str[i + 1]);
			const int lo = (hi >= 0) ? oidc_http_url_decode_hex_digit(str[i + 2]) : -1;
			if (lo >= 0) {
				*p++ = (char)((hi << 4) | lo);
				i += 3;
			} else {
				*p++ = c;
				i++;
			}
		} else {
			*p++ = c;
			i++;
		}
	}
	*p = '\0';

	return rv;
}

/*
 * obtain a HTTP request header value
 */
static const char *oidc_http_hdr_in_get(const request_rec *r, const char *name) {
	const char *value = apr_table_get(r->headers_in, name);
	if (value)
		oidc_debug(r, "%s=%s", name, value);
	return value;
}

/*
 * obtain the left-most element of a multi-valued HTTP header value
 */
static const char *oidc_http_hdr_in_get_left_most_only(const request_rec *r, const char *name, const char *separator) {
	char *last = NULL;
	const char *value = oidc_http_hdr_in_get(r, name);
	if (value)
		return apr_strtok(apr_pstrdup(r->pool, value), separator, &last);
	return NULL;
}

/*
 * check if a multi-valued HTTP request header contains a specified value
 */
static apr_byte_t oidc_http_hdr_in_contains(const request_rec *r, const char *name, const char *separator,
					    const char postfix_separator, const char *needle) {
	char *ctx = NULL;
	const char *elem = NULL;
	const char *value = oidc_http_hdr_in_get(r, name);
	apr_byte_t rc = FALSE;
	if (value) {
		elem = apr_strtok(apr_pstrdup(r->pool, value), separator, &ctx);
		while (elem != NULL) {
			while (*elem == OIDC_CHAR_SPACE)
				elem++;
			if ((_oidc_strncmp(elem, needle, _oidc_strlen(needle)) == 0) &&
			    ((elem[_oidc_strlen(needle)] == '\0') ||
			     (elem[_oidc_strlen(needle)] == postfix_separator))) {
				rc = TRUE;
				break;
			}
			elem = apr_strtok(NULL, separator, &ctx);
		}
	}
	return rc;
}

/*
 * copy a header value with CR/LF replaced, to prevent header injection
 */
static char *oidc_http_hdr_value_sanitize(const request_rec *r, const char *value) {
	char *s_value = apr_pstrdup(r->pool, value);
	char *p = NULL;
	while ((p = strpbrk(s_value, "\r\n")))
		*p = OIDC_CHAR_SPACE;
	return s_value;
}

/*
 * set a HTTP response header; table could be headers_out or err_headers_out
 */
static void oidc_http_hdr_table_set(const request_rec *r, apr_table_t *table, const char *name, const char *value) {

	if (value != NULL) {

		char *s_value = oidc_http_hdr_value_sanitize(r, value);

		oidc_debug(r, "%s: %s", name, s_value);
		apr_table_set(table, name, s_value);

	} else {

		oidc_debug(r, "unset %s", name);
		apr_table_unset(table, name);
	}
}

/*
 * append a (sanitized) header to a table without the scan for an existing entry that apr_table_set()
 * does, for callers that collect many headers and merge them into the request's table at once with
 * apr_table_overlap(); the name and the value must live as long as the table
 */
void oidc_http_hdr_table_add(const request_rec *r, apr_table_t *table, const char *name, const char *value) {
	char *s_value = oidc_http_hdr_value_sanitize(r, value);
	oidc_debug(r, "%s: %s", name, s_value);
	apr_table_addn(table, name, s_value);
}

/*
 * set a (regular) HTTP response header
 */
static void oidc_http_hdr_out_set(const request_rec *r, const char *name, const char *value) {
	oidc_http_hdr_table_set(r, r->headers_out, name, value);
}

/*
 * obtain a HTTP response header value
 */
static const char *oidc_http_hdr_out_get(const request_rec *r, const char *name) {
	return apr_table_get(r->headers_out, name);
}

/*
 * set a HTTP response header on all responses, included errors and redirects
 */
void oidc_http_hdr_err_out_add(const request_rec *r, const char *name, const char *value) {
	oidc_debug(r, "%s: %s", name, value);
	apr_table_add(r->err_headers_out, name, value);
}

/*
 * mark a response that may carry tokens or other sensitive material as non-cacheable,
 * on all responses including errors and redirects
 */
void oidc_http_set_no_cache_headers(const request_rec *r) {
	oidc_http_hdr_err_out_add(r, OIDC_HTTP_HDR_CACHE_CONTROL, "no-cache, no-store");
	oidc_http_hdr_err_out_add(r, OIDC_HTTP_HDR_PRAGMA, "no-cache");
}

/*
 * set an HTTP request header
 */
void oidc_http_hdr_in_set(const request_rec *r, const char *name, const char *value) {
	oidc_http_hdr_table_set(r, r->headers_in, name, value);
}

/*
 * obtain a HTTP request cookie value
 */
const char *oidc_http_hdr_in_cookie_get(const request_rec *r) {
	return oidc_http_hdr_in_get(r, OIDC_HTTP_HDR_COOKIE);
}

/*
 * set a HTTP request cookie value
 */
void oidc_http_hdr_in_cookie_set(const request_rec *r, const char *value) {
	oidc_http_hdr_in_set(r, OIDC_HTTP_HDR_COOKIE, value);
}

/*
 * obtain the User-Agent header value from the HTTP request
 */
const char *oidc_http_hdr_in_user_agent_get(const request_rec *r) {
	return oidc_http_hdr_in_get(r, OIDC_HTTP_HDR_USER_AGENT);
}

/*
 * obtain the X-Forwarded-For header value from the HTTP request
 */
const char *oidc_http_hdr_in_x_forwarded_for_get(const request_rec *r) {
	return oidc_http_hdr_in_get_left_most_only(r, OIDC_HTTP_HDR_X_FORWARDED_FOR, OIDC_STR_COMMA OIDC_STR_SPACE);
}

/*
 * obtain the Content-Type header value from the HTTP request
 */
const char *oidc_http_hdr_in_content_type_get(const request_rec *r) {
	return oidc_http_hdr_in_get(r, OIDC_HTTP_HDR_CONTENT_TYPE);
}

/*
 * obtain the Content-Length header value from the HTTP request
 */
const char *oidc_http_hdr_in_content_length_get(const request_rec *r) {
	return oidc_http_hdr_in_get(r, OIDC_HTTP_HDR_CONTENT_LENGTH);
}

/*
 * obtain the X-Requested-With header value from the HTTP request
 */
const char *oidc_http_hdr_in_x_requested_with_get(const request_rec *r) {
	return oidc_http_hdr_in_get(r, OIDC_HTTP_HDR_X_REQUESTED_WITH);
}

/*
 * obtain the Sec-Fetch-Mode header value from the HTTP request
 */
const char *oidc_http_hdr_in_sec_fetch_mode_get(const request_rec *r) {
	return oidc_http_hdr_in_get(r, OIDC_HTTP_HDR_SEC_FETCH_MODE);
}

/*
 * obtain the Sec-Fetch-Dest header value from the HTTP request
 */
const char *oidc_http_hdr_in_sec_fetch_dest_get(const request_rec *r) {
	return oidc_http_hdr_in_get(r, OIDC_HTTP_HDR_SEC_FETCH_DEST);
}

/*
 * obtain the Accept header value from the HTTP request
 */
const char *oidc_http_hdr_in_accept_get(const request_rec *r) {
	return oidc_http_hdr_in_get(r, OIDC_HTTP_HDR_ACCEPT);
}

/*
 * check if a specified value exists in the HTTP Accept request header
 */
apr_byte_t oidc_http_hdr_in_accept_contains(const request_rec *r, const char *needle) {
	return oidc_http_hdr_in_contains(r, OIDC_HTTP_HDR_ACCEPT, OIDC_STR_COMMA, OIDC_CHAR_SEMI_COLON, needle);
}

/*
 * obtain the Authorization header value from the HTTP request
 */
const char *oidc_http_hdr_in_authorization_get(const request_rec *r) {
	return oidc_http_hdr_in_get(r, OIDC_HTTP_HDR_AUTHORIZATION);
}

/*
 * obtain the X-Forwarded-Proto header value from the HTTP request
 */
const char *oidc_http_hdr_in_x_forwarded_proto_get(const request_rec *r) {
	return oidc_http_hdr_in_get_left_most_only(r, OIDC_HTTP_HDR_X_FORWARDED_PROTO, OIDC_STR_COMMA OIDC_STR_SPACE);
}

/*
 * obtain the X-Forwarded-Port header value from the HTTP request
 */
const char *oidc_http_hdr_in_x_forwarded_port_get(const request_rec *r) {
	return oidc_http_hdr_in_get_left_most_only(r, OIDC_HTTP_HDR_X_FORWARDED_PORT, OIDC_STR_COMMA OIDC_STR_SPACE);
}

/*
 * obtain the X-Forwarded-Host header value from the HTTP request
 */
const char *oidc_http_hdr_in_x_forwarded_host_get(const request_rec *r) {
	return oidc_http_hdr_in_get_left_most_only(r, OIDC_HTTP_HDR_X_FORWARDED_HOST, OIDC_STR_COMMA OIDC_STR_SPACE);
}

/*
 * obtain the Forwarded header value from the HTTP request
 */
const char *oidc_http_hdr_in_forwarded_get(const request_rec *r) {
	return oidc_http_hdr_in_get_left_most_only(r, OIDC_HTTP_HDR_FORWARDED, OIDC_STR_COMMA);
}

/*
 * obtain the Host header value from the HTTP request
 */
const char *oidc_http_hdr_in_host_get(const request_rec *r) {
	return oidc_http_hdr_in_get(r, OIDC_HTTP_HDR_HOST);
}

/*
 * obtain the traceparent header value from the HTTP request
 */
const char *oidc_http_hdr_in_traceparent_get(const request_rec *r) {
	return oidc_http_hdr_in_get(r, OIDC_HTTP_HDR_TRACE_PARENT);
}

/*
 * set the Location header value in the HTTP response
 */
void oidc_http_hdr_out_location_set(const request_rec *r, const char *value) {
	oidc_http_hdr_out_set(r, OIDC_HTTP_HDR_LOCATION, value);
}

/*
 * obtain the Location header value from the HTTP response
 */
const char *oidc_http_hdr_out_location_get(const request_rec *r) {
	return oidc_http_hdr_out_get(r, OIDC_HTTP_HDR_LOCATION);
}

/*
 * obtain a specified value from the Forwarded header in the HTTP request
 */
const char *oidc_http_hdr_forwarded_get(const request_rec *r, const char *elem) {
	const char *value = NULL;
	char *ptr = NULL;
	const char *item = apr_psprintf(r->pool, "%s=", elem);
	value = oidc_http_hdr_in_forwarded_get(r);
	value = oidc_util_strcasestr(value, item);
	if (value) {
		value += _oidc_strlen(item);
		ptr = _oidc_strstr(value, ";");
		if (ptr)
			*ptr = '\0';
		ptr = _oidc_strstr(value, " ");
		if (ptr)
			*ptr = '\0';
	}
	return value ? apr_pstrdup(r->pool, value) : NULL;
}

/*
 * normalize a string for use as an HTTP Header Name.  Any invalid
 * characters (per http://tools.ietf.org/html/rfc2616#section-4.2 and
 * http://tools.ietf.org/html/rfc2616#section-2.2) are replaced with
 * a dash ('-') character.
 */
char *oidc_http_hdr_normalize_name(const request_rec *r, const char *str) {
	/* token = 1*<any CHAR except CTLs or separators>
	 * CTL = <any US-ASCII control character
	 *          (octets 0 - 31) and DEL (127)>
	 * separators = "(" | ")" | "<" | ">" | "@"
	 *              | "," | ";" | ":" | "\" | <">
	 *              | "/" | "[" | "]" | "?" | "="
	 *              | "{" | "}" | SP | HT */
	const char *separators = "()<>@,;:\\\"/[]?={} \t";

	char *ns = apr_pstrdup(r->pool, str);
	const size_t len = _oidc_strlen(ns);
	for (size_t i = 0; i < len; i++) {
		if (ns[i] < 32 || ns[i] == 127)
			ns[i] = '-';
		else if (strchr(separators, ns[i]) != NULL)
			ns[i] = '-';
	}
	return ns;
}

/*
 * callback for CURL to write bytes that come back from an HTTP call
 */
size_t oidc_http_response_data(void *contents, size_t size, size_t nmemb, void *userp) {
	size_t realsize = size * nmemb;
	oidc_curl_resp_data_ctx_t *mem = (oidc_curl_resp_data_ctx_t *)userp;

	/* check if we don't run over the maximum buffer/memory size for HTTP responses */
	if (mem->size + realsize > OIDC_CURL_RESPONSE_DATA_SIZE_MAX) {
		oidc_error(
		    mem->r,
		    "HTTP response larger than maximum allowed size: current size=%ld, additional size=%ld, max=%d",
		    (long)mem->size, (long)realsize, OIDC_CURL_RESPONSE_DATA_SIZE_MAX);
		return 0;
	}

	/* allocate the new buffer for the current + new response bytes */
	char *newptr = apr_palloc(mem->r->pool, mem->size + realsize + 1);
	if (newptr == NULL) {
		oidc_error(mem->r, "memory allocation for new buffer of %ld bytes failed",
			   (long)(mem->size + realsize + 1));
		return 0;
	}

	/* copy over the data from current memory plus the cURL buffer */
	_oidc_memcpy(newptr, mem->memory, mem->size);
	_oidc_memcpy(&(newptr[mem->size]), contents, realsize);
	mem->size += realsize;
	mem->memory = newptr;
	mem->memory[mem->size] = 0;

	return realsize;
}

/*
 * callback for CURL to write response headers that come back from an HTTP call
 */
size_t oidc_http_response_header(const char *buffer, size_t size, size_t nitems, void *userdata) {
	/* received header is nitems * size long in 'buffer' NOT ZERO TERMINATED */
	oidc_curl_resp_hdr_ctx_t *ctx = (oidc_curl_resp_hdr_ctx_t *)userdata;
	const char *hdr = NULL;
	char *value = NULL;
	char *h_name = NULL;
	apr_ssize_t h_len = 0;
	int i = 0;

	/* see if there is a header to search for */
	if ((ctx->hdrs == NULL) || (apr_hash_count(ctx->hdrs) == 0))
		goto end;

	/* make hdr a \0 terminated string for easier processing */
	hdr = apr_pstrndup(ctx->r->pool, buffer, nitems * size);

	/* search for a name: value pair */
	value = _oidc_strstr(hdr, OIDC_STR_COLON);
	if (value == NULL)
		goto end;

	/* split the header name and value */
	*value = '\0';

	/* see if there's any header value characters at all after the colon */
	if (_oidc_strlen(hdr) < nitems * size) {
		value++;
		/* skip spaces after the colon */
		while (*value == ' ')
			value++;
		/* remove trailing /r/n */
		i = (int)_oidc_strlen(value) - 1;
		while ((i >= 0) && ((value[i] == '\r') || (value[i] == '\n')))
			value[i--] = '\0';
	}

	/* check if the caller is interested in the value of the current response header */
	for (apr_hash_index_t *hi = apr_hash_first(NULL, ctx->hdrs); hi; hi = apr_hash_next(hi)) {
		apr_hash_this(hi, (const void **)&h_name, &h_len, NULL);
		if (_oidc_strnatcasecmp(hdr, h_name) == 0) {
			oidc_debug(ctx->r, "returning response header: %s: %s", h_name, value);
			apr_hash_set(ctx->hdrs, h_name, APR_HASH_KEY_STRING, apr_pstrdup(ctx->r->pool, value));
			break;
		}
	}

end:

	return nitems * size;
}

/* context structure for encoding parameters */
typedef struct oidc_http_encode_t {
	request_rec *r;
	/* the encoded "key=value" fragments, joined once with "&" by the callers below; accumulating
	 * them into a growing string per parameter instead made the work quadratic in the number of
	 * parameters, which a request-controlled parameter count can turn into an OOM (OSS-Fuzz 551746349) */
	apr_array_header_t *elems;
} oidc_http_encode_t;

/*
 * names of protocol parameters that carry secrets or tokens and must be redacted
 * before a request URL/body is written to the debug log; the single source of truth
 * for both the per-parameter check and the body redaction below
 */
static const char *_oidc_http_sensitive_params[] = {OIDC_PROTO_CLIENT_SECRET, OIDC_PROTO_CLIENT_ASSERTION,
						    OIDC_PROTO_CODE, OIDC_PROTO_CODE_VERIFIER, OIDC_PROTO_REFRESH_TOKEN,
						    OIDC_PROTO_ACCESS_TOKEN,
						    /* inbound: the front-channel authorization response and the
						     * back-channel logout request carry these */
						    OIDC_PROTO_ID_TOKEN, OIDC_PROTO_LOGOUT_TOKEN, NULL};

apr_byte_t oidc_http_param_is_sensitive(const char *key) {
	for (int i = 0; _oidc_http_sensitive_params[i] != NULL; i++)
		if (_oidc_strcmp(key, _oidc_http_sensitive_params[i]) == 0)
			return TRUE;
	return FALSE;
}

/*
 * TRUE when the name_len-byte parameter name is one of _oidc_http_sensitive_params
 */
static apr_byte_t oidc_http_param_name_is_sensitive(const char *name, size_t name_len) {
	for (int i = 0; _oidc_http_sensitive_params[i] != NULL; i++)
		if ((_oidc_strlen(_oidc_http_sensitive_params[i]) == name_len) &&
		    (_oidc_strncmp(name, _oidc_http_sensitive_params[i], name_len) == 0))
			return TRUE;
	return FALSE;
}

/*
 * best-effort redaction of the well-known sensitive parameters listed in
 * _oidc_http_sensitive_params inside a URL-form-encoded request body, for debug-log
 * purposes; JSON request bodies do not carry these parameters in this codebase and
 * are therefore left untouched
 */
const char *oidc_http_redact_body_for_log(request_rec *r, const char *data) {
	size_t n_params = 1;

	if (data == NULL)
		return NULL;
	/* OIDCDebugMaskSecrets Off, or no request pool to build the redacted copy in */
	if ((r == NULL) || (oidc_util_log_mask_secrets(r) == FALSE))
		return data;
	apr_pool_t *pool = r->pool;

	/*
	 * one pass over the "&"-separated parameters into a single buffer: rewriting the whole
	 * string with an apr_psprintf() per redacted value made the work quadratic in the number
	 * of sensitive parameters, which a request-sized body turned into an OOM (OSS-Fuzz
	 * 554483423); a redacted value only ever grows to the 3 bytes of "***", so the result fits
	 * in the input length plus 3 bytes per parameter
	 */
	for (const char *c = data; *c != '\0'; c++)
		if (*c == OIDC_CHAR_AMP)
			n_params++;
	char *result = apr_palloc(pool, _oidc_strlen(data) + (3 * n_params) + 1);
	char *out = result;
	const char *param = data;

	for (;;) {
		const char *param_end = strchr(param, OIDC_CHAR_AMP);
		const size_t param_len = (param_end != NULL) ? (size_t)(param_end - param) : _oidc_strlen(param);
		const char *eq = memchr(param, OIDC_CHAR_EQUAL, param_len);
		/* only a "<name>=" at the parameter boundary is redacted, so that e.g. "xcode=" is not taken for
		 * "code=" */
		const apr_byte_t redact =
		    ((eq != NULL) && (oidc_http_param_name_is_sensitive(param, (size_t)(eq - param)) == TRUE)) ? TRUE
													       : FALSE;
		/* everything up to and including the "<name>=" is kept */
		const size_t keep = (redact == TRUE) ? (size_t)(eq - param) + 1 : param_len;

		_oidc_memcpy(out, param, keep);
		out += keep;
		if (redact == TRUE) {
			_oidc_memcpy(out, "***", 3);
			out += 3;
		}
		if (param_end == NULL)
			break;
		*out = OIDC_CHAR_AMP;
		out++;
		param = param_end + 1;
	}
	*out = '\0';

	return result;
}

/* Sensitive response members differ from the request parameters redacted above. */
static const char *_oidc_http_sensitive_json_members[] = {OIDC_PROTO_ACCESS_TOKEN,     OIDC_PROTO_REFRESH_TOKEN,
							  OIDC_PROTO_ID_TOKEN,	       OIDC_PROTO_CLIENT_SECRET,
							  "registration_access_token", NULL};

/*
 * Redact quoted JSON members without parsing, so malformed or non-JSON responses remain
 * loggable and otherwise unchanged. This scanner assumes a sensitive value has no escaped quote:
 * the next quote terminates the value.
 * The input is copied through into a single buffer with each value replaced as it is found:
 * rewriting the whole string with an apr_psprintf() per member made the work quadratic in the
 * number of sensitive members, which a response-sized body turned into an OOM (OSS-Fuzz
 * 554483423). A match consumes at least the member name, the colon and the opening quote and
 * adds at most the 3 bytes of "***", which bounds the size of the result.
 */
static const char *oidc_http_redact_json_member(apr_pool_t *pool, const char *data, const char *needle) {
	const apr_size_t needle_len = _oidc_strlen(needle);
	const apr_size_t data_len = _oidc_strlen(data);
	char *result = apr_palloc(pool, data_len + (3 * ((data_len / (needle_len + 2)) + 1)) + 1);
	char *out = result;
	/* the input up to here has been copied to the output */
	const char *copied = data;
	/* the scan cursor */
	const char *pos = data;
	size_t len = 0;

	for (;;) {
		const char *match = _oidc_strstr(pos, needle);
		if (match == NULL)
			break;
		/* step over the member name and expect ": " then a quoted value; anything
		 * else (a non-string value, a member name appearing inside a value) is
		 * skipped rather than guessed at */
		const char *p = match + needle_len;
		while ((*p == ' ') || (*p == '\t'))
			p++;
		if (*p != OIDC_CHAR_COLON) {
			pos = match + needle_len;
			continue;
		}
		p++;
		while ((*p == ' ') || (*p == '\t'))
			p++;
		if (*p != OIDC_CHAR_DQUOTE) {
			pos = match + needle_len;
			continue;
		}
		p++;
		/* copy everything up to and including the opening quote, then the mask */
		len = (size_t)(p - copied);
		_oidc_memcpy(out, copied, len);
		out += len;
		_oidc_memcpy(out, "***", 3);
		out += 3;
		/* If the closing quote is missing, mask to the end rather than leak a partial token; the
		 * closing quote itself is copied with the next chunk and the scan resumes after the mask. */
		const char *value_end = strchr(p, OIDC_CHAR_DQUOTE);
		copied = (value_end != NULL) ? value_end : p + _oidc_strlen(p);
		pos = copied;
	}

	len = _oidc_strlen(copied);
	_oidc_memcpy(out, copied, len);
	out += len;
	*out = '\0';

	return result;
}

const char *oidc_http_redact_json_for_log(request_rec *r, const char *data) {
	const char *result = NULL;

	if (data == NULL)
		return NULL;
	/* OIDCDebugMaskSecrets Off, or no request pool to build the redacted copy in */
	if ((r == NULL) || (oidc_util_log_mask_secrets(r) == FALSE))
		return data;
	apr_pool_t *pool = r->pool;

	result = data;
	for (int i = 0; _oidc_http_sensitive_json_members[i] != NULL; i++)
		result = oidc_http_redact_json_member(
		    pool, result, apr_pstrcat(pool, "\"", _oidc_http_sensitive_json_members[i], "\"", NULL));

	return result;
}

/*
 * add a url-form-encoded name/value pair
 */
static int oidc_http_add_form_url_encoded_param(void *rec, const char *key, const char *value) {
	oidc_http_encode_t *ctx = (oidc_http_encode_t *)rec;
	APR_ARRAY_PUSH(ctx->elems, const char *) =
	    apr_psprintf(ctx->r->pool, "%s=%s", oidc_http_url_encode(ctx->r, key), oidc_http_url_encode(ctx->r, value));
	return 1;
}

/*
 * add a name/value pair to a form-encoded string used only for debug logging, redacting
 * the value when the parameter name is a known secret/token
 */
static int oidc_http_add_form_encoded_param_for_log(void *rec, const char *key, const char *value) {
	oidc_http_encode_t *ctx = (oidc_http_encode_t *)rec;
	const char *safe_value = value ? value : "";
	const char *v = oidc_http_param_is_sensitive(key) ? "***" : safe_value;
	APR_ARRAY_PUSH(ctx->elems, const char *) = apr_psprintf(ctx->r->pool, "%s=%s", key, v);
	return 1;
}

/*
 * run one of the per-parameter encoders above over a table and join the collected "key=value"
 * fragments into a single "&"-separated string in one allocation; returns NULL when there are none
 */
static char *oidc_http_encode_params(request_rec *r, const apr_table_t *params,
				     int (*encoder)(void *, const char *, const char *)) {
	oidc_http_encode_t ctx = {r, apr_array_make(r->pool, apr_table_elts(params)->nelts, sizeof(const char *))};
	apr_table_do(encoder, &ctx, params, NULL);
	return (ctx.elems->nelts > 0) ? apr_array_pstrcat(r->pool, ctx.elems, OIDC_CHAR_AMP) : NULL;
}

/*
 * construct a URL with query parameters
 */
char *oidc_http_query_encoded_url(request_rec *r, const char *url, const apr_table_t *params) {
	char *result = NULL;
	if (url == NULL) {
		oidc_error(r, "URL is NULL");
		return NULL;
	}
	if ((params != NULL) && (apr_table_elts(params)->nelts > 0)) {
		const char *encoded_params = oidc_http_encode_params(r, params, oidc_http_add_form_url_encoded_param);
		const char *sep = NULL;
		if (encoded_params)
			sep = strchr(url, OIDC_CHAR_QUERY) != NULL ? OIDC_STR_AMP : OIDC_STR_QUERY;
		result = apr_psprintf(r->pool, "%s%s%s", url, sep ? sep : "", encoded_params ? encoded_params : "");

		const char *log_params = oidc_http_encode_params(r, params, oidc_http_add_form_encoded_param_for_log);
		oidc_debug(r, "url=%s%s%s", url, sep ? sep : "", log_params ? log_params : "");
	} else {
		result = apr_pstrdup(r->pool, url);
		oidc_debug(r, "url=%s", result);
	}
	return result;
}

/*
 * construct form-encoded POST data
 */
char *oidc_http_form_encoded_data(request_rec *r, const apr_table_t *params) {
	char *data = NULL;
	if ((params != NULL) && (apr_table_elts(params)->nelts > 0)) {
		data = oidc_http_encode_params(r, params, oidc_http_add_form_url_encoded_param);

		const char *log_params = oidc_http_encode_params(r, params, oidc_http_add_form_encoded_param_for_log);
		oidc_debug(r, "data=%s", log_params ? log_params : "(null)");
	} else {
		oidc_debug(r, "data=(null)");
	}
	return data;
}

/*
 * call curl_easy_setopt with error checking and reporting
 */
#define OIDC_HTTP_CURL_SETOPT_PARMS(r, curl, code, option, ...)                                                        \
	code = curl_easy_setopt(curl, option, __VA_ARGS__);                                                            \
	if (code != CURLE_OK)                                                                                          \
	oidc_error(r, "curl_easy_setopt(%s) failed with: %s", #option, curl_easy_strerror(code))

#define OIDC_HTTP_CURL_SETOPT(...) OIDC_HTTP_CURL_SETOPT_PARMS(r, curl, code, __VA_ARGS__)

/*
 * set libcurl SSL options
 */

#define OIDC_HTTP_CURL_SETOPT_SSL(option, value)                                                                       \
	if (_oidc_strstr(env_var_value, #value) != NULL) {                                                             \
		oidc_debug(r, "curl_easy_setopt(%s): %s (%ld)", #option, #value, (long)value);                         \
		OIDC_HTTP_CURL_SETOPT(option, (long)value);                                                            \
	}

static void oidc_http_set_curl_ssl_options(request_rec *r, CURL *curl) {
	// NB: the variable names r, curl, code and env_var_value are used in the OIDC_HTTP_CURL_SETOPT_SSL macro
	const char *env_var_value = NULL;
	CURLcode code = CURLE_OK;
	if (r->subprocess_env != NULL)
		env_var_value = apr_table_get(r->subprocess_env, OIDC_CURLOPT_SSL_OPTIONS_ENV_VAR_NAME);
	if (env_var_value == NULL)
		return;
	oidc_debug(r, "SSL options environment variable %s=%s found", OIDC_CURLOPT_SSL_OPTIONS_ENV_VAR_NAME,
		   env_var_value);
#if LIBCURL_VERSION_NUM >= 0x071900
	OIDC_HTTP_CURL_SETOPT_SSL(CURLOPT_SSL_OPTIONS, CURLSSLOPT_ALLOW_BEAST);
#endif
#if LIBCURL_VERSION_NUM >= 0x072c00
	OIDC_HTTP_CURL_SETOPT_SSL(CURLOPT_SSL_OPTIONS, CURLSSLOPT_NO_REVOKE)
#endif
#if LIBCURL_VERSION_NUM >= 0x074400
	OIDC_HTTP_CURL_SETOPT_SSL(CURLOPT_SSL_OPTIONS, CURLSSLOPT_NO_PARTIALCHAIN)
#endif
#if LIBCURL_VERSION_NUM >= 0x074600
	OIDC_HTTP_CURL_SETOPT_SSL(CURLOPT_SSL_OPTIONS, CURLSSLOPT_REVOKE_BEST_EFFORT)
#endif
#if LIBCURL_VERSION_NUM >= 0x074700
	OIDC_HTTP_CURL_SETOPT_SSL(CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA)
#endif
#if LIBCURL_VERSION_NUM >= 0x072200
	OIDC_HTTP_CURL_SETOPT_SSL(CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_0)
	OIDC_HTTP_CURL_SETOPT_SSL(CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_1)
	OIDC_HTTP_CURL_SETOPT_SSL(CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2)
#endif
#if LIBCURL_VERSION_NUM >= 0x073400
	OIDC_HTTP_CURL_SETOPT_SSL(CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_3)
#endif
#if LIBCURL_VERSION_NUM >= 0x073600
	OIDC_HTTP_CURL_SETOPT_SSL(CURLOPT_SSLVERSION, CURL_SSLVERSION_MAX_TLSv1_0)
	OIDC_HTTP_CURL_SETOPT_SSL(CURLOPT_SSLVERSION, CURL_SSLVERSION_MAX_TLSv1_1)
	OIDC_HTTP_CURL_SETOPT_SSL(CURLOPT_SSLVERSION, CURL_SSLVERSION_MAX_TLSv1_2)
	OIDC_HTTP_CURL_SETOPT_SSL(CURLOPT_SSLVERSION, CURL_SSLVERSION_MAX_TLSv1_3)
#endif
}

/*
 * construct our User-Agent header for outgoing requests
 */
const char *oidc_http_user_agent(request_rec *r) {
	const char *s_useragent = apr_table_get(r->subprocess_env, OIDC_USER_AGENT_ENV_VAR);
	if (s_useragent == NULL) {
		s_useragent = apr_psprintf(r->pool, "[%s:%u:%lu] %s", r->server->server_hostname,
					   r->connection->local_addr->port, (unsigned long)getpid(), NAMEVERSION);
		s_useragent = apr_psprintf(r->pool, "%s libcurl-%s %s", s_useragent, LIBCURL_VERSION,
					   oidc_util_openssl_version(r->pool));
	}
	return s_useragent;
}

/*
 * construct our local address/interface for outgoing requests
 */
const char *oidc_http_interface(const request_rec *r) {
	return apr_table_get(r->subprocess_env, OIDC_CURL_INTERFACE_ENV_VAR);
}

/*
 * configure the CA bundle for SSL server certificate verification, falling
 * back to the system curl-ca-bundle.crt on Windows when no explicit bundle
 * was configured
 */
static void oidc_http_request_setup_ca_bundle(request_rec *r, CURL *curl, const oidc_cfg_t *c) {
	// NB: the variable names r, curl, and code are used in the OIDC_HTTP_CURL_SETOPT macro
	CURLcode code = CURLE_OK;

	if (oidc_cfg_ca_bundle_path_get(c) != NULL) {
		OIDC_HTTP_CURL_SETOPT(CURLOPT_CAINFO, oidc_cfg_ca_bundle_path_get(c));
		return;
	}

#ifdef WIN32
	DWORD buflen;
	char *ptr = NULL;
	char *retval = (char *)malloc(sizeof(TCHAR) * (MAX_PATH + 1));
	retval[0] = '\0';
	buflen = SearchPath(NULL, "curl-ca-bundle.crt", NULL, MAX_PATH + 1, retval, &ptr);
	if (buflen > 0) {
		OIDC_HTTP_CURL_SETOPT(CURLOPT_CAINFO, retval);
	} else {
		oidc_warn(r, "no curl-ca-bundle.crt file found in path");
	}
	free(retval);
#endif
}

/*
 * configure the curl handle to use the optional outgoing proxy, including
 * its credentials and authentication type when supplied
 */
static void oidc_http_request_setup_proxy(request_rec *r, CURL *curl,
					  const oidc_http_outgoing_proxy_t *outgoing_proxy) {
	// NB: the variable names r, curl, and code are used in the OIDC_HTTP_CURL_SETOPT macro
	CURLcode code = CURLE_OK;

	if (outgoing_proxy->host_port == NULL)
		return;

	OIDC_HTTP_CURL_SETOPT(CURLOPT_PROXY, outgoing_proxy->host_port);
	if (outgoing_proxy->username_password) {
		OIDC_HTTP_CURL_SETOPT(CURLOPT_PROXYUSERPWD, outgoing_proxy->username_password);
	}
	if (outgoing_proxy->auth_type != (unsigned long)OIDC_CONFIG_POS_INT_UNSET) {
		OIDC_HTTP_CURL_SETOPT(CURLOPT_PROXYAUTH, outgoing_proxy->auth_type);
	}
}

/*
 * build the list of custom request headers (authorization, content-type,
 * traceparent, DPoP) to pass on to curl
 */
struct curl_slist *oidc_http_request_build_header_list(request_rec *r, const oidc_cfg_t *c, const char *content_type,
						       const char *access_token, const char *dpop) {
	struct curl_slist *h_list = NULL;

	/* see if we need to add token in the Bearer/DPoP Authorization header */
	if (access_token != NULL)
		h_list = curl_slist_append(h_list, apr_psprintf(r->pool, "%s: %s %s", OIDC_HTTP_HDR_AUTHORIZATION,
								dpop ? "DPoP" : "Bearer", access_token));

	if (content_type != NULL)
		h_list = curl_slist_append(h_list,
					   apr_psprintf(r->pool, "%s: %s", OIDC_HTTP_HDR_CONTENT_TYPE, content_type));

	const char *traceparent = oidc_http_hdr_in_traceparent_get(r);
	if (traceparent && oidc_cfg_trace_parent_get(c) != OIDC_TRACE_PARENT_OFF) {
		oidc_debug(r, "propagating traceparent header: %s", traceparent);
		h_list =
		    curl_slist_append(h_list, apr_psprintf(r->pool, "%s: %s", OIDC_HTTP_HDR_TRACE_PARENT, traceparent));
	}

	if (dpop != NULL) {
		oidc_debug(r, "appending DPoP header (len=%d)", (int)_oidc_strlen(dpop));
		h_list = curl_slist_append(h_list, apr_psprintf(r->pool, "%s: %s", OIDC_HTTP_HDR_DPOP, dpop));
	}

	return h_list;
}

/*
 * pass cookies from the incoming request through to the curl handle by
 * concatenating the configured cookies into a single Cookie header value
 */
static void oidc_http_request_pass_cookies(request_rec *r, CURL *curl, const apr_array_header_t *pass_cookies) {
	// NB: the variable names r, curl, and code are used in the OIDC_HTTP_CURL_SETOPT macro
	CURLcode code = CURLE_OK;
	char *cookie_string = NULL;

	if (pass_cookies == NULL)
		return;

	for (int i = 0; i < pass_cookies->nelts; i++) {
		const char *cookie_name = APR_ARRAY_IDX(pass_cookies, i, const char *);
		char *cookie_value = oidc_http_get_cookie(r, cookie_name);
		if (cookie_value == NULL)
			continue;
		cookie_string = (cookie_string == NULL)
				    ? apr_psprintf(r->pool, "%s=%s", cookie_name, cookie_value)
				    : apr_psprintf(r->pool, "%s; %s=%s", cookie_string, cookie_name, cookie_value);
	}

	if (cookie_string == NULL)
		return;

	oidc_debug(r, "passing browser cookies on backend call: %s", cookie_string);
	OIDC_HTTP_CURL_SETOPT(CURLOPT_COOKIE, cookie_string);
}

/*
 * execute the curl request honoring the configured retry policy; retries
 * are skipped on a request/transfer timeout and short-circuited on success
 */
static apr_byte_t oidc_http_request_perform_with_retries(request_rec *r, const oidc_cfg_t *c, CURL *curl,
							 const char *url, const char *curl_err,
							 const oidc_http_timeout_t *http_timeout) {
	CURLcode res = CURLE_OK;

	for (int i = 0; i <= http_timeout->retries; i++) {
		res = curl_easy_perform(curl);
		if (res == CURLE_OK)
			return TRUE;
		if (res == CURLE_OPERATION_TIMEDOUT) {
			/* in case of a request/transfer timeout (which includes the connect timeout) we'll not retry */
			oidc_error(r, "curl_easy_perform failed with a timeout for %s: [%s]; won't retry", url,
				   curl_err[0] ? curl_err : "<n/a>");
			OIDC_METRICS_COUNTER_INC_VALUE(r, c, OM_PROVIDER_CONNECT_ERROR,
						       curl_err[0] ? curl_err : "timeout");
			return FALSE;
		}
		oidc_error(r, "curl_easy_perform(%d/%d) failed for %s with: [%s]", i + 1, http_timeout->retries + 1,
			   url, curl_err[0] ? curl_err : "<n/a>");
		OIDC_METRICS_COUNTER_INC_VALUE(r, c, OM_PROVIDER_CONNECT_ERROR, curl_err[0] ? curl_err : "undefined");
		/* in case of a connectivity/network glitch we'll back off before retrying */
		if (i < http_timeout->retries)
			apr_sleep(apr_time_from_msec(http_timeout->retry_interval));
	}

	return FALSE;
}

/*
 * execute a HTTP (GET or POST) request
 */
static apr_byte_t oidc_http_request(request_rec *r, const char *url, const char *data, const char *content_type,
				    const char *basic_auth, const char *access_token, const char *dpop,
				    int ssl_validate_server, char **response, long *response_code,
				    apr_hash_t *response_hdrs, const oidc_http_timeout_t *http_timeout,
				    const oidc_http_outgoing_proxy_t *outgoing_proxy,
				    const apr_array_header_t *pass_cookies, const char *ssl_cert, const char *ssl_key,
				    const char *ssl_key_pwd) {

	// NB: the variable names r, curl, and code are used in the OIDC_HTTP_CURL_SETOPT macro
	CURL *curl = NULL;
	CURLcode code = CURLE_OK;
	char curl_err[CURL_ERROR_SIZE];
	oidc_curl_resp_data_ctx_t d_buf = {r, NULL, 0};
	oidc_curl_resp_hdr_ctx_t h_buf = {r, response_hdrs};
	struct curl_slist *h_list = NULL;
	long http_code = 0;
	apr_byte_t rv = FALSE;
	const oidc_cfg_t *c = ap_get_module_config(r->server->module_config, &auth_openidc_module);

	/* do some logging about the inputs */
	oidc_debug(r,
		   "url=%s, data=%s, content_type=%s, basic_auth=%s, access_token=%s, dpop=%s, ssl_validate_server=%d, "
		   "request_timeout=%d, connect_timeout=%d, retries=%d, retry_interval=%d, outgoing_proxy=%s:%s:%d, "
		   "pass_cookies=%pp, ssl_cert=%s, ssl_key=%s, ssl_key_pwd=%s",
		   url, oidc_http_redact_body_for_log(r, data), content_type, basic_auth ? "****" : "null",
		   oidc_util_mask_value(r, access_token), dpop, ssl_validate_server, http_timeout->request_timeout,
		   http_timeout->connect_timeout, http_timeout->retries, http_timeout->retry_interval,
		   outgoing_proxy->host_port, outgoing_proxy->username_password ? "****" : "(null)",
		   (int)outgoing_proxy->auth_type, pass_cookies, ssl_cert, ssl_key, ssl_key_pwd ? "****" : "(null)");

	/* an endpoint that is not configured is a caller error, not a transfer failure: fail here rather
	 * than hand curl a NULL URL and then back off and retry a request that can never be sent */
	if (url == NULL) {
		oidc_error(r, "no URL to send the request to: the endpoint is not configured");
		return FALSE;
	}

	curl = oidc_http_curl_acquire();
	if (curl == NULL) {
		oidc_error(r, "could not obtain a curl handle");
		goto end;
	}

	/* set the error buffer as empty before performing a request */
	curl_err[0] = 0;

	/* some of these are not really required */
	OIDC_HTTP_CURL_SETOPT(CURLOPT_HEADER, 0L);
	OIDC_HTTP_CURL_SETOPT(CURLOPT_NOPROGRESS, 1L);
	OIDC_HTTP_CURL_SETOPT(CURLOPT_NOSIGNAL, 1L);
	/* keep reused connections alive across the (potentially long) intervals between calls */
	OIDC_HTTP_CURL_SETOPT(CURLOPT_TCP_KEEPALIVE, 1L);
	OIDC_HTTP_CURL_SETOPT(CURLOPT_ERRORBUFFER, curl_err);
	OIDC_HTTP_CURL_SETOPT(CURLOPT_FOLLOWLOCATION, 1L);
	OIDC_HTTP_CURL_SETOPT(CURLOPT_MAXREDIRS, 5L);

	/* set the timeouts */
	OIDC_HTTP_CURL_SETOPT(CURLOPT_TIMEOUT, (long)http_timeout->request_timeout);
	OIDC_HTTP_CURL_SETOPT(CURLOPT_CONNECTTIMEOUT, (long)http_timeout->connect_timeout);

	/* setup the buffer where the response data will be written to */
	OIDC_HTTP_CURL_SETOPT(CURLOPT_WRITEFUNCTION, oidc_http_response_data);
	/* coverity[bad_sizeof] */
	OIDC_HTTP_CURL_SETOPT(CURLOPT_WRITEDATA, &d_buf);

	/* setup the buffer where the response headers will be written to */
	OIDC_HTTP_CURL_SETOPT(CURLOPT_HEADERFUNCTION, oidc_http_response_header);
	/* coverity[bad_sizeof] */
	OIDC_HTTP_CURL_SETOPT(CURLOPT_HEADERDATA, &h_buf);

#ifndef LIBCURL_NO_CURLPROTO
#if LIBCURL_VERSION_NUM >= 0x075500
	OIDC_HTTP_CURL_SETOPT(CURLOPT_REDIR_PROTOCOLS_STR, "http,https");
	OIDC_HTTP_CURL_SETOPT(CURLOPT_PROTOCOLS_STR, "http,https");
#else
	OIDC_HTTP_CURL_SETOPT(CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
	OIDC_HTTP_CURL_SETOPT(CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
#endif
#endif

	/* set the options for validating the SSL server certificate that the remote site presents */
	OIDC_HTTP_CURL_SETOPT(CURLOPT_SSL_VERIFYPEER, (ssl_validate_server != FALSE ? 1L : 0L));
	OIDC_HTTP_CURL_SETOPT(CURLOPT_SSL_VERIFYHOST, (ssl_validate_server != FALSE ? 2L : 0L));

	oidc_http_set_curl_ssl_options(r, curl);

	oidc_http_request_setup_ca_bundle(r, curl, c);

	/* identify this HTTP client */
	const char *s_useragent = oidc_http_user_agent(r);
	if ((s_useragent != NULL) && (_oidc_strcmp(s_useragent, "") != 0)) {
		oidc_debug(r, "set HTTP request header User-Agent to: %s", s_useragent);
		OIDC_HTTP_CURL_SETOPT(CURLOPT_USERAGENT, s_useragent);
	}

	/* set the local interface if defined */
	const char *s_interface = oidc_http_interface(r);
	if ((s_interface != NULL) && (_oidc_strcmp(s_interface, "") != 0)) {
#if LIBCURL_VERSION_NUM >= 0x073000
		oidc_debug(r, "set local interface to: %s", s_interface);
		OIDC_HTTP_CURL_SETOPT(CURLOPT_INTERFACE, s_interface);
#else
		oidc_warn(
		    r, "local interface is configured to %s, but the cURL version in use does not support setting this",
		    s_interface);
#endif
	}

	oidc_http_request_setup_proxy(r, curl, outgoing_proxy);

	/* see if we need to perform HTTP basic authentication to the remote site */
	if (basic_auth != NULL) {
		OIDC_HTTP_CURL_SETOPT(CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
		OIDC_HTTP_CURL_SETOPT(CURLOPT_USERPWD, basic_auth);
	}

	if (ssl_cert != NULL) {
		OIDC_HTTP_CURL_SETOPT(CURLOPT_SSLCERT, ssl_cert);
	}
	if (ssl_key != NULL) {
		OIDC_HTTP_CURL_SETOPT(CURLOPT_SSLKEY, ssl_key);
	}
	if (ssl_key_pwd != NULL) {
		OIDC_HTTP_CURL_SETOPT(CURLOPT_KEYPASSWD, ssl_key_pwd);
	}

	if (data != NULL) {
		/* set POST data and switch HTTP method to POST */
		OIDC_HTTP_CURL_SETOPT(CURLOPT_POSTFIELDS, data);
		OIDC_HTTP_CURL_SETOPT(CURLOPT_POST, 1L);
	}

	h_list = oidc_http_request_build_header_list(r, c, content_type, access_token, dpop);
	if (h_list != NULL) {
		OIDC_HTTP_CURL_SETOPT(CURLOPT_HTTPHEADER, h_list);
	}

	oidc_http_request_pass_cookies(r, curl, pass_cookies);

	/* set the target URL */
	OIDC_HTTP_CURL_SETOPT(CURLOPT_URL, url);

	/* call it and record the result */
	rv = oidc_http_request_perform_with_retries(r, c, curl, url, curl_err, http_timeout);
	if (rv == FALSE)
		goto end;

	curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
	oidc_debug(r, "HTTP response code=%ld", http_code);

	OIDC_METRICS_COUNTER_INC_VALUE(r, c, OM_PROVIDER_HTTP_RESPONSE_CODE, apr_psprintf(r->pool, "%ld", http_code));

	*response = apr_pstrmemdup(r->pool, d_buf.memory, d_buf.size);
	if (response_code)
		*response_code = http_code;

	/* set and log the response; the token, introspection and registration responses carry
	 * credentials, so redact them the way the request body above already is */
	oidc_debug(r, "response=%s", oidc_http_redact_json_for_log(r, *response ? *response : ""));

end:

	/* cleanup and return the result; a handle that performed successfully goes back to the
	 * pool with its connection kept alive for the next request to the same endpoint */
	if (h_list != NULL)
		curl_slist_free_all(h_list);
	oidc_http_curl_release(curl, rv);

	return rv;
}

/*
 * execute HTTP GET request
 */
apr_byte_t oidc_http_get(request_rec *r, const char *url, const apr_table_t *params, const char *basic_auth,
			 const char *access_token, const char *dpop, int ssl_validate_server, char **response,
			 long *response_code, apr_hash_t *response_hdrs, const oidc_http_timeout_t *http_timeout,
			 const oidc_http_outgoing_proxy_t *outgoing_proxy, const apr_array_header_t *pass_cookies,
			 const char *ssl_cert, const char *ssl_key, const char *ssl_key_pwd) {
	const char *query_url = oidc_http_query_encoded_url(r, url, params);
	return oidc_http_request(r, query_url, NULL, NULL, basic_auth, access_token, dpop, ssl_validate_server,
				 response, response_code, response_hdrs, http_timeout, outgoing_proxy, pass_cookies,
				 ssl_cert, ssl_key, ssl_key_pwd);
}

/*
 * execute HTTP POST request with form-encoded data
 */
apr_byte_t oidc_http_post_form(request_rec *r, const char *url, const apr_table_t *params, const char *basic_auth,
			       const char *access_token, const char *dpop, int ssl_validate_server, char **response,
			       long *response_code, apr_hash_t *response_hdrs, const oidc_http_timeout_t *http_timeout,
			       const oidc_http_outgoing_proxy_t *outgoing_proxy, const apr_array_header_t *pass_cookies,
			       const char *ssl_cert, const char *ssl_key, const char *ssl_key_pwd) {
	const char *data = oidc_http_form_encoded_data(r, params);
	return oidc_http_request(r, url, data, OIDC_HTTP_CONTENT_TYPE_FORM_ENCODED, basic_auth, access_token, dpop,
				 ssl_validate_server, response, response_code, response_hdrs, http_timeout,
				 outgoing_proxy, pass_cookies, ssl_cert, ssl_key, ssl_key_pwd);
}

/*
 * execute HTTP POST request with JSON-encoded data
 */
apr_byte_t oidc_http_post_json(request_rec *r, const char *url, const oidc_json_t *json, const char *basic_auth,
			       const char *access_token, const char *dpop, int ssl_validate_server, char **response,
			       long *response_code, apr_hash_t *response_hdrs, const oidc_http_timeout_t *http_timeout,
			       const oidc_http_outgoing_proxy_t *outgoing_proxy, const apr_array_header_t *pass_cookies,
			       const char *ssl_cert, const char *ssl_key, const char *ssl_key_pwd) {
	const char *data =
	    json != NULL ? oidc_json_encode(r->pool, json, OIDC_JSON_PRESERVE_ORDER | OIDC_JSON_COMPACT) : NULL;
	return oidc_http_request(r, url, data, OIDC_HTTP_CONTENT_TYPE_JSON, basic_auth, access_token, dpop,
				 ssl_validate_server, response, response_code, response_hdrs, http_timeout,
				 outgoing_proxy, pass_cookies, ssl_cert, ssl_key, ssl_key_pwd);
}

/*
 * get the current path from the request in a normalized way
 */
static char *oidc_http_get_path(request_rec *r) {
	size_t i;
	const char *p;
	p = r->parsed_uri.path;
	if ((p == NULL) || (p[0] == '\0'))
		return apr_pstrdup(r->pool, OIDC_STR_FORWARD_SLASH);
	for (i = _oidc_strlen(p) - 1; i > 0; i--)
		if (p[i] == OIDC_CHAR_FORWARD_SLASH)
			break;
	return apr_pstrndup(r->pool, p, i + 1);
}

/*
 * get the cookie path setting and check that it matches the request path; cook it up if it is not set
 */
static const char *oidc_http_get_cookie_path(request_rec *r) {
	const char *rv = NULL;
	char *requestPath = oidc_http_get_path(r);
	const char *cookie_path = oidc_cfg_dir_cookie_path_get(r);
	if (cookie_path != NULL) {
		if (_oidc_strncmp(cookie_path, requestPath, _oidc_strlen(cookie_path)) == 0)
			rv = cookie_path;
		else {
			oidc_warn(r,
				  "" OIDCCookiePath
				  " (%s) is not a substring of request path, using request path (%s) for cookie",
				  cookie_path, requestPath);
			rv = requestPath;
		}
	} else {
		rv = requestPath;
	}
	return rv;
}

#define OIDC_HTTP_COOKIE_FLAG_DOMAIN "Domain"
#define OIDC_HTTP_COOKIE_FLAG_PATH "Path"
#define OIDC_HTTP_COOKIE_FLAG_EXPIRES "Expires"
#define OIDC_HTTP_COOKIE_FLAG_SECURE "Secure"
#define OIDC_HTTP_COOKIE_FLAG_HTTP_ONLY "HttpOnly"

#define OIDC_HTTP_COOKIE_MAX_SIZE 4093

#define OIDC_SET_COOKIE_APPEND_ENV_VAR "OIDC_SET_COOKIE_APPEND"

/*
 * obtain the value configured in the OIDC_SET_COOKIE_APPEND environment variable
 * which is to be added to the HTTP Set-Cookie response header
 */
static const char *oidc_http_set_cookie_append_value(request_rec *r) {
	const char *env_var_value = NULL;

	if (r->subprocess_env != NULL)
		env_var_value = apr_table_get(r->subprocess_env, OIDC_SET_COOKIE_APPEND_ENV_VAR);

	if (env_var_value == NULL) {
		oidc_debug(r, "no cookie append environment variable %s found", OIDC_SET_COOKIE_APPEND_ENV_VAR);
		return NULL;
	}

	oidc_debug(r, "cookie append environment variable %s=%s found", OIDC_SET_COOKIE_APPEND_ENV_VAR, env_var_value);

	return env_var_value;
}

/*
 * set a cookie in the HTTP response headers
 */
void oidc_http_set_cookie(request_rec *r, const char *cookieName, const char *cookieValue, apr_time_t expires,
			  const char *ext) {

	const oidc_cfg_t *c = ap_get_module_config(r->server->module_config, &auth_openidc_module);
	char *headerString = NULL;
	char *expiresString = NULL;
	const char *appendString = NULL;

	/* see if we need to clear the cookie */
	if (_oidc_strcmp(cookieValue, "") == 0)
		expires = 0;

	/* construct the expire value */
	if (expires != -1) {
		expiresString = (char *)apr_pcalloc(r->pool, APR_RFC822_DATE_LEN);
		if (apr_rfc822_date(expiresString, expires) != APR_SUCCESS) {
			oidc_error(r, "could not set cookie expiry date");
		}
	}

	/* construct the cookie value */
	headerString = apr_psprintf(r->pool, "%s=%s", cookieName, cookieValue);

	headerString =
	    apr_psprintf(r->pool, "%s; %s=%s", headerString, OIDC_HTTP_COOKIE_FLAG_PATH, oidc_http_get_cookie_path(r));

	if (expiresString != NULL)
		headerString =
		    apr_psprintf(r->pool, "%s; %s=%s", headerString, OIDC_HTTP_COOKIE_FLAG_EXPIRES, expiresString);

	if (oidc_cfg_cookie_domain_get(c) != NULL)
		headerString = apr_psprintf(r->pool, "%s; %s=%s", headerString, OIDC_HTTP_COOKIE_FLAG_DOMAIN,
					    oidc_cfg_cookie_domain_get(c));

	if (oidc_util_url_cur_is_secure(r, c))
		headerString = apr_psprintf(r->pool, "%s; %s", headerString, OIDC_HTTP_COOKIE_FLAG_SECURE);

	if (oidc_cfg_cookie_http_only_get(c) != FALSE)
		headerString = apr_psprintf(r->pool, "%s; %s", headerString, OIDC_HTTP_COOKIE_FLAG_HTTP_ONLY);

	appendString = oidc_http_set_cookie_append_value(r);
	if (appendString != NULL)
		headerString = apr_psprintf(r->pool, "%s; %s", headerString, appendString);
	else if (ext != NULL)
		headerString = apr_psprintf(r->pool, "%s; %s", headerString, ext);

	/* sanity check on overall cookie value size */
	if (_oidc_strlen(headerString) > OIDC_HTTP_COOKIE_MAX_SIZE) {
		oidc_warn(r,
			  "the length of the cookie value (%d) is greater than %d(!) bytes, this may not work "
			  "with all browsers/server combinations: consider switching to a server side caching!",
			  (int)_oidc_strlen(headerString), OIDC_HTTP_COOKIE_MAX_SIZE);
	}

	/* use r->err_headers_out so we always print our headers (even on 302 redirect) - headers_out only
	 * prints on 2xx responses */
	oidc_http_hdr_err_out_add(r, OIDC_HTTP_HDR_SET_COOKIE, headerString);
}

/*
 * get a cookie from the HTTP request
 */
char *oidc_http_get_cookie(request_rec *r, const char *cookieName) {
	const char *cookie = NULL;
	char *tokenizerCtx = NULL;
	char *rv = NULL;

	/* get the Cookie value */
	char *cookies = apr_pstrdup(r->pool, oidc_http_hdr_in_cookie_get(r));

	if (cookies != NULL) {

		/* tokenize on ; to find the cookie we want */
		cookie = apr_strtok(cookies, OIDC_STR_SEMI_COLON, &tokenizerCtx);

		while (cookie != NULL) {

			while (*cookie == OIDC_CHAR_SPACE)
				cookie++;

			/* see if we've found the cookie that we're looking for */
			if ((_oidc_strncmp(cookie, cookieName, _oidc_strlen(cookieName)) == 0) &&
			    (cookie[_oidc_strlen(cookieName)] == OIDC_CHAR_EQUAL)) {

				/* skip to the meat of the parameter (the value after the '=') */
				cookie += (_oidc_strlen(cookieName) + 1);
				rv = apr_pstrdup(r->pool, cookie);

				break;
			}

			/* go to the next cookie */
			cookie = apr_strtok(NULL, OIDC_STR_SEMI_COLON, &tokenizerCtx);
		}
	}

	/* log what we've found */
	/* in client-cookie session mode the value is the whole session credential */
	oidc_debug(r, "returning \"%s\" = %s", cookieName,
		   rv ? apr_psprintf(r->pool, "\"%s\"", oidc_util_mask_value(r, rv)) : "<null>");

	return rv;
}

#define OIDC_HTTP_COOKIE_CHUNKS_SEPARATOR "_"
#define OIDC_HTTP_COOKIE_CHUNKS_POSTFIX "chunks"

/* the largest number of chunks a chunked cookie may be split over; the counter cookie holds it as
 * a decimal number, so this bounds both what is written and what is accepted on the way back in */
#define OIDC_HTTP_COOKIE_CHUNKS_MAX 99

/*
 * get the name of the cookie that contains the number of chunks
 */
static char *oidc_http_get_chunk_count_name(request_rec *r, const char *cookieName) {
	return apr_psprintf(r->pool, "%s%s%s", cookieName, OIDC_HTTP_COOKIE_CHUNKS_SEPARATOR,
			    OIDC_HTTP_COOKIE_CHUNKS_POSTFIX);
}

/*
 * get the number of cookie chunks set by the browser
 */
static int oidc_http_get_chunked_count(request_rec *r, const char *cookieName) {
	int chunkCount = 0;
	const char *chunkCountValue = oidc_http_get_cookie(r, oidc_http_get_chunk_count_name(r, cookieName));
	chunkCount = _oidc_str_to_int(chunkCountValue, 0);
	return chunkCount;
}

/*
 * get the name of a chunk
 */
static char *oidc_http_get_chunk_cookie_name(request_rec *r, const char *cookieName, int i) {
	return apr_psprintf(r->pool, "%s%s%d", cookieName, OIDC_HTTP_COOKIE_CHUNKS_SEPARATOR, i);
}

/*
 * get a cookie value that is split over a number of chunked cookies
 */
char *oidc_http_get_chunked_cookie(request_rec *r, const char *cookieName, int chunkSize) {
	char *cookieValue = NULL;
	char *chunkValue = NULL;
	int chunkCount = 0;
	if (chunkSize == 0)
		return oidc_http_get_cookie(r, cookieName);
	chunkCount = oidc_http_get_chunked_count(r, cookieName);
	if (chunkCount == 0)
		return oidc_http_get_cookie(r, cookieName);
	if ((chunkCount < 0) || (chunkCount > OIDC_HTTP_COOKIE_CHUNKS_MAX)) {
		oidc_warn(r, "chunk count out of bounds: %d", chunkCount);
		return NULL;
	}
	for (int i = 0; i < chunkCount; i++) {
		chunkValue = oidc_http_get_cookie(r, oidc_http_get_chunk_cookie_name(r, cookieName, i));
		if (chunkValue == NULL) {
			/* refuse a partial assembly: a truncated value would fail session decode (or worse)
			 * and leave the browser stuck resubmitting the broken cookie set */
			oidc_warn(r, "could not find chunk %d; aborting", i);
			return NULL;
		}
		cookieValue = apr_psprintf(r->pool, "%s%s", cookieValue ? cookieValue : "", chunkValue);
	}
	return cookieValue;
}

/*
 * unset all chunked cookies, including the counter cookie, if they exist
 */
static void oidc_http_clear_chunked_cookie(request_rec *r, const char *cookieName, apr_time_t expires,
					   const char *ext) {
	int chunkCount = oidc_http_get_chunked_count(r, cookieName);
	if (chunkCount > 0) {
		for (int i = 0; i < chunkCount; i++)
			oidc_http_set_cookie(r, oidc_http_get_chunk_cookie_name(r, cookieName, i), "", expires, ext);
		oidc_http_set_cookie(r, oidc_http_get_chunk_count_name(r, cookieName), "", expires, ext);
	}
}

/*
 * set a cookie value that is split over a number of chunked cookies
 */
apr_byte_t oidc_http_set_chunked_cookie(request_rec *r, const char *cookieName, const char *cookieValue,
					apr_time_t expires, int chunkSize, const char *ext) {
	int cookieLength = (int)_oidc_strlen(cookieValue);
	const char *chunkValue = NULL;

	/* see if we need to chunk at all */
	if ((chunkSize == 0) || ((cookieLength > 0) && (cookieLength < chunkSize))) {
		oidc_http_set_cookie(r, cookieName, cookieValue, expires, ext);
		oidc_http_clear_chunked_cookie(r, cookieName, expires, ext);
		return TRUE;
	}

	/* see if we need to clear a possibly chunked cookie */
	if (cookieLength == 0) {
		oidc_http_set_cookie(r, cookieName, "", expires, ext);
		oidc_http_clear_chunked_cookie(r, cookieName, expires, ext);
		return TRUE;
	}

	/* Use ceil division without advertising a trailing empty chunk for exact multiples. */
	int chunkCountValue = (cookieLength + chunkSize - 1) / chunkSize;

	/* refuse to write what oidc_http_get_chunked_cookie would refuse to read back: writing it
	 * anyway leaves the browser holding a value that is dropped on the next request, which for a
	 * client-side session means authenticating over and over rather than a visible failure */
	if (chunkCountValue > OIDC_HTTP_COOKIE_CHUNKS_MAX) {
		oidc_error(r,
			   "cookie \"%s\" would have to be split over %d chunks, which is more than the maximum of "
			   "%d; increase " OIDCSessionCookieChunkSize " (currently %d) or store the session in a "
			   "server-side cache with " OIDCSessionType " \"server-cache\"",
			   cookieName, chunkCountValue, OIDC_HTTP_COOKIE_CHUNKS_MAX, chunkSize);
		return FALSE;
	}

	const char *ptr = cookieValue;
	for (int i = 0; i < chunkCountValue; i++) {
		chunkValue = apr_pstrndup(r->pool, ptr, chunkSize);
		ptr += chunkSize;
		oidc_http_set_cookie(r, oidc_http_get_chunk_cookie_name(r, cookieName, i), chunkValue, expires, ext);
	}
	oidc_http_set_cookie(r, oidc_http_get_chunk_count_name(r, cookieName),
			     apr_psprintf(r->pool, "%d", chunkCountValue), expires, ext);
	oidc_http_set_cookie(r, cookieName, "", expires, ext);

	return TRUE;
}

/*
 * construct the HTTP outgoing proxy options
 */
const char **oidc_http_proxy_auth_options(void) {
	static const char *options[] = {OIDC_HTTP_PROXY_AUTH_BASIC,
					OIDC_HTTP_PROXY_AUTH_DIGEST,
					OIDC_HTTP_PROXY_AUTH_NTLM,
					OIDC_HTTP_PROXY_AUTH_ANY,
#ifdef CURLAUTH_NEGOTIATE
					OIDC_HTTP_PROXY_AUTH_NEGOTIATE,
#endif
					NULL};
	return options;
}

/*
 * return the CURL enum value for the HTTP outgoing proxy options
 */
unsigned long oidc_http_proxy_s2auth(const char *arg) {
	if (_oidc_strcmp(arg, OIDC_HTTP_PROXY_AUTH_BASIC) == 0)
		return CURLAUTH_BASIC;
	if (_oidc_strcmp(arg, OIDC_HTTP_PROXY_AUTH_DIGEST) == 0)
		return CURLAUTH_DIGEST;
	if (_oidc_strcmp(arg, OIDC_HTTP_PROXY_AUTH_NTLM) == 0)
		return CURLAUTH_NTLM;
	if (_oidc_strcmp(arg, OIDC_HTTP_PROXY_AUTH_ANY) == 0)
		return CURLAUTH_ANY;
#ifdef CURLAUTH_NEGOTIATE
	if (_oidc_strcmp(arg, OIDC_HTTP_PROXY_AUTH_NEGOTIATE) == 0)
		return CURLAUTH_NEGOTIATE;
#endif
	return CURLAUTH_NONE;
}

/*
 * initialize the HTTP/cURL environment
 */
void oidc_http_init(void) {
	curl_global_init(CURL_GLOBAL_ALL);
}

/*
 * clean up the HTTP/cURL environment
 */
void oidc_http_cleanup(void) {
	curl_global_cleanup();
}

/* Pool reset handles to retain their connection, DNS, and TLS caches. */
#define OIDC_HTTP_CURL_POOL_MAX 16

static CURL *_oidc_http_curl_pool[OIDC_HTTP_CURL_POOL_MAX];
static int _oidc_http_curl_pool_num = 0;
static apr_byte_t _oidc_http_curl_pool_enabled = FALSE;
#if APR_HAS_THREADS
static apr_thread_mutex_t *_oidc_http_curl_pool_mutex = NULL;
#endif

static void oidc_http_curl_pool_lock(void) {
#if APR_HAS_THREADS
	apr_thread_mutex_lock(_oidc_http_curl_pool_mutex);
#endif
}

static void oidc_http_curl_pool_unlock(void) {
#if APR_HAS_THREADS
	apr_thread_mutex_unlock(_oidc_http_curl_pool_mutex);
#endif
}

static apr_status_t oidc_http_curl_pool_cleanup(void *data) {
	for (int i = 0; i < _oidc_http_curl_pool_num; i++)
		curl_easy_cleanup(_oidc_http_curl_pool[i]);
	_oidc_http_curl_pool_num = 0;
	_oidc_http_curl_pool_enabled = FALSE;
#if APR_HAS_THREADS
	_oidc_http_curl_pool_mutex = NULL;
#endif
	return APR_SUCCESS;
}

void oidc_http_curl_pool_init(apr_pool_t *pool) {
	if (_oidc_http_curl_pool_enabled == TRUE)
		return;
#if APR_HAS_THREADS
	if (apr_thread_mutex_create(&_oidc_http_curl_pool_mutex, APR_THREAD_MUTEX_DEFAULT, pool) != APR_SUCCESS)
		return;
#endif
	_oidc_http_curl_pool_num = 0;
	_oidc_http_curl_pool_enabled = TRUE;
	apr_pool_cleanup_register(pool, NULL, oidc_http_curl_pool_cleanup, apr_pool_cleanup_null);
}

/*
 * forget any handles inherited over fork(): their connections share file descriptors and TLS
 * state with the parent, so the child must neither use nor curl_easy_cleanup them (a cleanup
 * would write a TLS close-notify into a socket the parent may still be using)
 */
void oidc_http_curl_pool_child_init(void) {
	_oidc_http_curl_pool_num = 0;
}

static CURL *oidc_http_curl_acquire(void) {
	CURL *curl = NULL;
	if (_oidc_http_curl_pool_enabled == FALSE)
		return curl_easy_init();
	oidc_http_curl_pool_lock();
	if (_oidc_http_curl_pool_num > 0)
		curl = _oidc_http_curl_pool[--_oidc_http_curl_pool_num];
	oidc_http_curl_pool_unlock();
	return (curl != NULL) ? curl : curl_easy_init();
}

/*
 * return a handle to the pool after a successful request: the reset clears its options but
 * keeps the connection/DNS/TLS-session caches; a handle whose request failed is destroyed
 * rather than reused so a poisoned connection state cannot carry over
 */
static void oidc_http_curl_release(CURL *curl, apr_byte_t reuse) {
	if (curl == NULL)
		return;
	if ((reuse == TRUE) && (_oidc_http_curl_pool_enabled == TRUE)) {
		curl_easy_reset(curl);
		oidc_http_curl_pool_lock();
		if (_oidc_http_curl_pool_num < OIDC_HTTP_CURL_POOL_MAX) {
			_oidc_http_curl_pool[_oidc_http_curl_pool_num++] = curl;
			curl = NULL;
		}
		oidc_http_curl_pool_unlock();
	}
	if (curl != NULL)
		curl_easy_cleanup(curl);
}
