/*
 * 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.
 *
 * caching using a Redis backend
 *
 * @Author: Hans Zandbelt - hans.zandbelt@openidc.com
 */

#include "cache/redis.h"
#include "cfg/cfg_int.h"
#include "metrics.h"

#define REDIS_CONNECT_TIMEOUT_DEFAULT 5
#define REDIS_TIMEOUT_DEFAULT 5
#define REDIS_KEEPALIVE_DEFAULT -1

#define OIDC_REDIS_MUTEX_GLOBAL_ENV_VAR "OIDC_REDIS_MUTEX_GLOBAL"

/* create the cache context */
static oidc_cache_cfg_redis_t *oidc_cache_redis_cfg_create(apr_pool_t *pool) {
	oidc_cache_cfg_redis_t *context = apr_pcalloc(pool, sizeof(oidc_cache_cfg_redis_t));
	context->mutex = oidc_cache_mutex_create(pool, getenv(OIDC_REDIS_MUTEX_GLOBAL_ENV_VAR) ? TRUE : FALSE);
	context->username = NULL;
	context->passwd = NULL;
	context->database = -1;
	context->connect_timeout.tv_sec = REDIS_CONNECT_TIMEOUT_DEFAULT;
	context->connect_timeout.tv_usec = 0;
	context->keepalive = REDIS_KEEPALIVE_DEFAULT;
	context->timeout.tv_sec = REDIS_TIMEOUT_DEFAULT;
	context->timeout.tv_usec = 0;
	context->host_str = NULL;
	context->port = 0;
	context->rctx = NULL;
	/*
	 * Derived backends start in serialized mode and leave this false. Plain Redis post-config
	 * enables request-scoped connections unless OIDC_REDIS_MUTEX_GLOBAL selects the legacy mode.
	 */
	context->request_scoped = FALSE;
	context->idle_num = 0;
	return context;
}

int oidc_cache_redis_post_config(apr_pool_t *pool, server_rec *s, oidc_cfg_t *cfg, const char *name) {
	oidc_cache_cfg_redis_t *context = oidc_cache_redis_cfg_create(pool);
	cfg->cache.cfg = context;

	/* check that a Redis server has been configured */
	if (cfg->cache.redis_server == NULL) {
		oidc_serror(s, "cache type is set to \"%s\", but no valid " OIDCRedisCacheServer " setting was found",
			    name);
		return HTTP_INTERNAL_SERVER_ERROR;
	}

	if (cfg->cache.redis_username != NULL) {
		context->username = apr_pstrdup(pool, cfg->cache.redis_username);
	}
	if (cfg->cache.redis_password != NULL) {
		context->passwd = apr_pstrdup(pool, cfg->cache.redis_password);
	}

	if (oidc_cfg_cache_redis_database_get(cfg) != OIDC_CONFIG_POS_INT_UNSET)
		context->database = oidc_cfg_cache_redis_database_get(cfg);

	if (oidc_cfg_cache_redis_connect_timeout_get(cfg) != OIDC_CONFIG_POS_INT_UNSET)
		context->connect_timeout.tv_sec = oidc_cfg_cache_redis_connect_timeout_get(cfg);

	if (oidc_cfg_cache_redis_keepalive_get(cfg) != OIDC_CONFIG_POS_INT_UNSET)
		context->keepalive = oidc_cfg_cache_redis_keepalive_get(cfg);

	if (oidc_cfg_cache_redis_timeout_get(cfg) != OIDC_CONFIG_POS_INT_UNSET)
		context->timeout.tv_sec = oidc_cfg_cache_redis_timeout_get(cfg);

	if (oidc_cache_mutex_post_config(pool, s, context->mutex, name) == FALSE)
		return HTTP_INTERNAL_SERVER_ERROR;

	return OK;
}

static apr_status_t oidc_cache_redis_connect(request_rec *r, oidc_cache_cfg_redis_t *context);

/*
 * free resources allocated for the per-process Redis connection context
 */
apr_status_t oidc_cache_redis_disconnect(oidc_cache_cfg_redis_t *context) {
	if ((context != NULL) && (context->rctx != NULL)) {
		redisFree(context->rctx);
		context->rctx = NULL;
	}
	return APR_SUCCESS;
}

/*
 * return a copy of a (comma-separated) OIDCRedisCacheServer value with the credentials part of
 * each tuple - everything up to the last '@' in it - replaced by a placeholder, so that a
 * password configured there is never written to the log, not even when the value failed to parse
 */
const char *oidc_cache_redis_redact(apr_pool_t *pool, const char *str) {
	const char *rv = NULL;
	const char *tuple = NULL;
	char *token = NULL;
	const char *at = NULL;

	if (str == NULL)
		return NULL;

	/* tokenize a copy: apr_strtok modifies its input in place */
	tuple = apr_strtok(apr_pstrdup(pool, str), ",", &token);
	while (tuple != NULL) {
		at = strrchr(tuple, '@');
		rv = apr_pstrcat(pool, (rv != NULL) ? rv : "", (rv != NULL) ? "," : "",
				 (at != NULL) ? "<credentials>@" : "", (at != NULL) ? at + 1 : tuple, NULL);
		tuple = apr_strtok(NULL, ",", &token);
	}

	return (rv != NULL) ? rv : "";
}

/*
 * the oidc_cache_t post_config hook: initialize the offline config through
 * oidc_cache_redis_post_config (which validates that a server is configured),
 * then parse the server address and wire the connecting Redis operations
 */
static int oidc_cache_redis_post_config_hook(apr_pool_t *pool, server_rec *s) {
	apr_status_t rv = APR_SUCCESS;
	oidc_cache_cfg_redis_t *context = NULL;
	oidc_cfg_t *cfg = (oidc_cfg_t *)ap_get_module_config(s->module_config, &auth_openidc_module);

	if (cfg->cache.cfg != NULL)
		return OK;

	if (oidc_cache_redis_post_config(pool, s, cfg, "redis") != OK)
		return HTTP_INTERNAL_SERVER_ERROR;

	context = (oidc_cache_cfg_redis_t *)cfg->cache.cfg;

	/* parse the host:port tuple from the configuration */
	char *scope_id;
	rv = apr_parse_addr_port(&context->host_str, &scope_id, &context->port, cfg->cache.redis_server, pool);
	if (rv != APR_SUCCESS) {
		oidc_serror(s, "failed to parse cache server: '%s'",
			    oidc_cache_redis_redact(pool, cfg->cache.redis_server));
		return HTTP_INTERNAL_SERVER_ERROR;
	}

	if (context->host_str == NULL) {
		oidc_serror(s, "failed to parse cache server, no hostname specified: '%s'",
			    oidc_cache_redis_redact(pool, cfg->cache.redis_server));
		return HTTP_INTERNAL_SERVER_ERROR;
	}

	if (context->port == 0)
		context->port = 6379;

	context->connect = oidc_cache_redis_connect;
	context->command = oidc_cache_redis_command;
	context->disconnect = oidc_cache_redis_disconnect;
	context->request_scoped = getenv(OIDC_REDIS_MUTEX_GLOBAL_ENV_VAR) ? FALSE : TRUE;

	return OK;
}

/*
 * initialize the Redis cache in a child process
 */
int oidc_cache_redis_child_init(apr_pool_t *p, server_rec *s) {
	oidc_cfg_t *cfg = ap_get_module_config(s->module_config, &auth_openidc_module);
	oidc_cache_cfg_redis_t *context = (oidc_cache_cfg_redis_t *)cfg->cache.cfg;

	oidc_slog(s, APLOG_TRACE1, "init: %pp (t=%s, m=%pp, s=%pp, g=%d)", context, cfg->cache.impl->name,
		  context ? context->mutex : 0, s, (context && context->mutex) ? context->mutex->is_global : -1);

	/* initialize the lock for the child process */
	return oidc_cache_mutex_child_init(p, s, context->mutex);
}

/*
 * assemble single key name based on section/key input
 */
/*
 * free and nullify a reply object
 */
static void oidc_cache_redis_reply_free(redisReply **reply) {
	if (*reply != NULL) {
		freeReplyObject(*reply);
		*reply = NULL;
	}
}

apr_byte_t oidc_cache_redis_set_keepalive(request_rec *r, redisContext *rctx, const int keepalive) {
	apr_byte_t rv = TRUE;

	// default is -1
	if (keepalive == 0) {
		oidc_debug(r, "not setting redisEnableKeepAlive");
		goto end;
	}

#if HIREDIS_MAJOR >= 1 && HIREDIS_MINOR >= 2

	if (keepalive == -1) {
		oidc_debug(r, "setting redisEnableKeepAlive to the default interval");
		if (redisEnableKeepAlive(rctx) != REDIS_OK) {
			oidc_error(r, "redisEnableKeepAlive failed: %s", rctx->errstr);
			rv = FALSE;
		}
		goto end;
	}

	oidc_debug(r, "setting redisEnableKeepAliveWithInterval: %d", keepalive);
	if (redisEnableKeepAliveWithInterval(rctx, keepalive) != REDIS_OK) {
		oidc_error(r, "redisEnableKeepAliveWithInterval failed: %s", rctx->errstr);
		rv = FALSE;
	}

#else

	// -1 or > 0
	oidc_debug(r, "setting redisEnableKeepAlive to the default interval");
	if (redisEnableKeepAlive(rctx) != REDIS_OK) {
		oidc_error(r, "redisEnableKeepAlive failed: %s", rctx->errstr);
		rv = FALSE;
	}

#endif

end:

	return rv;
}

apr_byte_t oidc_cache_redis_set_auth(request_rec *r, redisContext *rctx, const char *username, const char *password) {
	apr_byte_t rv = TRUE;
	redisReply *reply = NULL;

	if (password == NULL)
		goto end;

	if (username != NULL)
		reply = redisCommand(rctx, "AUTH %s %s", username, password);
	else
		reply = redisCommand(rctx, "AUTH %s", password);

	if ((reply == NULL) || (reply->type == REDIS_REPLY_ERROR)) {
		oidc_error(r, "Redis AUTH command failed: '%s' [%s]", rctx->errstr, reply ? reply->str : "<n/a>");
		rv = FALSE;
		goto end;
	}

	oidc_debug(r, "successfully authenticated to the Redis server: %s", reply ? reply->str : "<n/a>");

end:

	oidc_cache_redis_reply_free(&reply);

	return rv;
}

apr_byte_t oidc_cache_redis_set_database(request_rec *r, redisContext *rctx, const int database) {
	apr_byte_t rv = TRUE;
	redisReply *reply = NULL;

	if (database == -1)
		goto end;

	reply = redisCommand(rctx, "SELECT %d", database);
	if ((reply == NULL) || (reply->type == REDIS_REPLY_ERROR)) {
		oidc_error(r, "Redis SELECT command failed: '%s' [%s]", rctx->errstr, reply ? reply->str : "<n/a>");
		rv = FALSE;
		goto end;
	}

	oidc_debug(r, "successfully selected database %d on the Redis server: %s", database,
		   reply ? reply->str : "<n/a>");

end:

	oidc_cache_redis_reply_free(&reply);

	return rv;
}

redisContext *oidc_cache_redis_connect_with_timeout(request_rec *r, const char *host, int port, struct timeval ct,
						    struct timeval t, const char *msg) {
	redisContext *rctx = NULL;

	oidc_debug(r, "calling redisConnectWithTimeout: %d", (int)ct.tv_sec);
	rctx = redisConnectWithTimeout(host, port, ct);

	if ((rctx == NULL) || (rctx->err != 0)) {
		oidc_error(r, "failed to connect to Redis server (%s%s%s:%d): '%s'", msg ? msg : "", msg ? ":" : "",
			   host, port, rctx != NULL ? rctx->errstr : "");
		if (rctx)
			redisFree(rctx);
		return NULL;
	}

	oidc_debug(r, "successfully connected to Redis server (%s%s%s:%d)", msg ? msg : "", msg ? ":" : "", host, port);

	if (redisSetTimeout(rctx, t) != REDIS_OK)
		oidc_error(r, "redisSetTimeout failed: %s", rctx->errstr);

	return rctx;
}

#define OIDC_CACHE_REDIS_CONN_USERDATA "oidc_cache_redis_conn"

/* the connection a request checked out of the idle pool; returned at request-pool cleanup */
typedef struct oidc_cache_redis_conn_t {
	redisContext *rctx;
	oidc_cache_cfg_redis_t *context;
	server_rec *s;
	apr_pool_t *pool;
} oidc_cache_redis_conn_t;

/* request-pool cleanup: put a healthy connection back on the idle pool, drop a broken one */
static apr_status_t oidc_cache_redis_conn_return(void *data) {
	oidc_cache_redis_conn_t *conn = (oidc_cache_redis_conn_t *)data;
	oidc_cache_cfg_redis_t *context = conn->context;

	if (conn->rctx == NULL)
		return APR_SUCCESS;

	if ((conn->rctx->err == 0) && (oidc_cache_mutex_lock(conn->pool, conn->s, context->mutex) == TRUE)) {
		if (context->idle_num < OIDC_CACHE_REDIS_POOL_MAX) {
			context->idle[context->idle_num++] = conn->rctx;
			conn->rctx = NULL;
		}
		oidc_cache_mutex_unlock(conn->pool, conn->s, context->mutex);
	}

	if (conn->rctx != NULL) {
		redisFree(conn->rctx);
		conn->rctx = NULL;
	}

	return APR_SUCCESS;
}

/* obtain the request's connection checkout, creating (and registering) it on first use */
static oidc_cache_redis_conn_t *oidc_cache_redis_conn_get(request_rec *r, oidc_cache_cfg_redis_t *context) {
	oidc_cache_redis_conn_t *conn = NULL;
	apr_pool_userdata_get((void **)&conn, OIDC_CACHE_REDIS_CONN_USERDATA, r->pool);
	if (conn == NULL) {
		conn = apr_pcalloc(r->pool, sizeof(oidc_cache_redis_conn_t));
		conn->context = context;
		conn->s = r->server;
		conn->pool = r->pool;
		apr_pool_userdata_set(conn, OIDC_CACHE_REDIS_CONN_USERDATA, NULL, r->pool);
		apr_pool_cleanup_register(r->pool, conn, oidc_cache_redis_conn_return, apr_pool_cleanup_null);
	}
	return conn;
}

/* establish (and authenticate) a fresh connection into the provided slot */
static apr_status_t oidc_cache_redis_rctx_connect(request_rec *r, const oidc_cache_cfg_redis_t *context,
						  redisContext **rctx) {

	apr_status_t rv = APR_EGENERAL;

	*rctx = oidc_cache_redis_connect_with_timeout(r, context->host_str, context->port, context->connect_timeout,
						      context->timeout, NULL);
	if (*rctx == NULL)
		goto end;

	if (oidc_cache_redis_set_keepalive(r, *rctx, context->keepalive) == FALSE)
		goto end;

	if (oidc_cache_redis_set_auth(r, *rctx, context->username, context->passwd) == FALSE)
		goto end;

	if (oidc_cache_redis_set_database(r, *rctx, context->database) == FALSE)
		goto end;

	rv = APR_SUCCESS;

end:

	if ((rv != APR_SUCCESS) && (*rctx != NULL)) {
		redisFree(*rctx);
		*rctx = NULL;
	}

	return rv;
}

/*
 * connect to Redis server
 */
static apr_status_t oidc_cache_redis_connect(request_rec *r, oidc_cache_cfg_redis_t *context) {

	if (context->request_scoped == FALSE) {

		/* legacy model: one shared connection, serialized by the caller-held mutex */
		if (context->rctx != NULL)
			return APR_SUCCESS;

		return oidc_cache_redis_rctx_connect(r, context, &context->rctx);
	}

	oidc_cache_redis_conn_t *conn = oidc_cache_redis_conn_get(r, context);

	if (conn->rctx != NULL) {
		if (conn->rctx->err == 0)
			return APR_SUCCESS;
		/* the checked-out connection failed earlier in this request: drop and reconnect */
		redisFree(conn->rctx);
		conn->rctx = NULL;
	}

	/* prefer an idle pooled connection over establishing (and authenticating) a fresh one */
	if (oidc_cache_mutex_lock(r->pool, r->server, context->mutex) == TRUE) {
		if (context->idle_num > 0)
			conn->rctx = context->idle[--context->idle_num];
		oidc_cache_mutex_unlock(r->pool, r->server, context->mutex);
	}

	if (conn->rctx != NULL) {
		if (conn->rctx->err == 0)
			return APR_SUCCESS;
		redisFree(conn->rctx);
		conn->rctx = NULL;
	}

	return oidc_cache_redis_rctx_connect(r, context, &conn->rctx);
}

/* Flush the LIFO idle pool when checkout finds a dead connection; older sockets are likely dead too. */
static void oidc_cache_redis_idle_flush(request_rec *r, oidc_cache_cfg_redis_t *context) {
	if (oidc_cache_mutex_lock(r->pool, r->server, context->mutex) != TRUE)
		return;
	if (context->idle_num > 0)
		oidc_debug(r, "dropping %d idle pooled connection(s)", context->idle_num);
	while (context->idle_num > 0)
		redisFree(context->idle[--context->idle_num]);
	oidc_cache_mutex_unlock(r->pool, r->server, context->mutex);
}

redisReply *oidc_cache_redis_command(request_rec *r, oidc_cache_cfg_redis_t *context, char **errstr, const char *format,
				     va_list ap) {
	redisReply *reply = NULL;
	redisContext *rctx = context->rctx;

	if (context->request_scoped == TRUE)
		rctx = oidc_cache_redis_conn_get(r, context)->rctx;

	if (rctx == NULL) {
		*errstr = apr_pstrdup(r->pool, "not connected");
		return NULL;
	}

	reply = redisvCommand(rctx, format, ap);
	*errstr = apr_pstrdup(r->pool, rctx->errstr);

	/* in request-scoped mode the (no request context) disconnect callback is a no-op, so a
	 * connection-level failure tears the checked-out connection down right here; the retry
	 * in oidc_cache_redis_exec then reconnects through oidc_cache_redis_connect */
	if ((context->request_scoped == TRUE) && (rctx->err != 0)) {
		oidc_cache_redis_conn_t *conn = oidc_cache_redis_conn_get(r, context);
		redisFree(conn->rctx);
		conn->rctx = NULL;
		/* A peer may close every idle socket without setting err; flush them before retrying. */
		oidc_cache_redis_idle_flush(r, context);
	}

	return reply;
}

static int oidc_cache_redis_env2int(const request_rec *r, const char *env_var_name, const int default_value) {
	const char *s = r->subprocess_env ? apr_table_get(r->subprocess_env, env_var_name) : NULL;
	return _oidc_str_to_int(s, default_value);
}

#define OIDC_REDIS_MAX_TRIES_ENV_VAR "OIDC_REDIS_MAX_TRIES"
#define OIDC_REDIS_MAX_TRIES_DEFAULT 2

#define OIDC_REDIS_RETRY_INTERVAL_ENV_VAR "OIDC_REDIS_RETRY_INTERVAL"
#define OIDC_REDIS_RETRY_INTERVAL_DEFAULT 300

#define OIDC_REDIS_WARN_OR_ERROR(cond, r, ...)                                                                         \
	if (cond) {                                                                                                    \
		oidc_warn(r, ##__VA_ARGS__);                                                                           \
	} else {                                                                                                       \
		oidc_error(r, ##__VA_ARGS__);                                                                          \
	}

/* Release the shared-connection mutex during retry delay, then reacquire it before returning. */
static apr_byte_t oidc_cache_redis_backoff(request_rec *r, const oidc_cache_cfg_redis_t *context, apr_time_t interval) {
	oidc_debug(r, "wait before retrying: %" APR_TIME_T_FMT " (msec)", apr_time_as_msec(interval));
	if (context->request_scoped == FALSE)
		oidc_cache_mutex_unlock(r->pool, r->server, context->mutex);
	apr_sleep(interval);
	if (context->request_scoped == FALSE)
		return oidc_cache_mutex_lock(r->pool, r->server, context->mutex);
	return TRUE;
}

/*
 * Serialized callers hold context->mutex. Retries release it while sleeping and clear
 * *mutex_held if reacquisition fails, preventing a second unlock of the shared connection.
 */
static redisReply *oidc_cache_redis_exec(request_rec *r, oidc_cache_cfg_redis_t *context, apr_byte_t *mutex_held,
					 const char *format, ...) {

	redisReply *reply = NULL;
	char *errstr = NULL;
	va_list ap;
	const oidc_cfg_t *cfg = ap_get_module_config(r->server->module_config, &auth_openidc_module);
	int retries = oidc_cache_redis_env2int(r, OIDC_REDIS_MAX_TRIES_ENV_VAR, OIDC_REDIS_MAX_TRIES_DEFAULT);
	apr_time_t interval = apr_time_from_msec(
	    oidc_cache_redis_env2int(r, OIDC_REDIS_RETRY_INTERVAL_ENV_VAR, OIDC_REDIS_RETRY_INTERVAL_DEFAULT));

	/* try to execute a command at max n times while reconnecting */
	for (int i = 1; i <= retries; i++) {

		if (i > 1)
			OIDC_METRICS_COUNTER_INC(r, cfg, OM_CACHE_RETRY);

		/* connect */
		if (context->connect(r, context) != APR_SUCCESS) {
			OIDC_REDIS_WARN_OR_ERROR(i < retries, r, "Redis connect (attempt=%d/%d to %s:%d) failed", i,
						 retries, context->host_str, context->port);
			if ((i < retries) && (oidc_cache_redis_backoff(r, context, interval) == FALSE)) {
				*mutex_held = FALSE;
				return NULL;
			}
			continue;
		}

		va_start(ap, format);
		/* execute the actual command */
		reply = context->command(r, context, &errstr, format, ap);
		va_end(ap);

		/* check for errors, need to return error replies for cache miss case REDIS_REPLY_NIL */
		if ((reply != NULL) && (reply->type != REDIS_REPLY_ERROR))
			/* break the loop and return the reply */
			break;

		/* something went wrong, log it */
		OIDC_REDIS_WARN_OR_ERROR(
		    i < retries, r, "Redis command (attempt=%d/%d to %s:%d) failed, disconnecting: '%s' [%s]", i,
		    retries, context->host_str, context->port, errstr, reply ? reply->str : "<n/a>");

		/* free the reply (if there is one allocated) */
		oidc_cache_redis_reply_free(&reply);

		/* cleanup, we may try again (once) after reconnecting */
		context->disconnect(context);
	}

	return reply;
}

/*
 * get a name/value pair from Redis
 */
apr_byte_t oidc_cache_redis_get(request_rec *r, const char *section, const char *key, char **value) {

	oidc_cfg_t *cfg = ap_get_module_config(r->server->module_config, &auth_openidc_module);
	oidc_cache_cfg_redis_t *context = (oidc_cache_cfg_redis_t *)cfg->cache.cfg;
	redisReply *reply = NULL;
	apr_byte_t rv = FALSE;
	apr_byte_t mutex_held = TRUE;

	/* grab the process lock, except in request-scoped connection mode where each request
	 * has its own connection and the mutex only guards the idle pool */
	if ((context->request_scoped == FALSE) && (oidc_cache_mutex_lock(r->pool, r->server, context->mutex) == FALSE))
		return FALSE;

	/* get */
	reply = oidc_cache_redis_exec(r, context, &mutex_held, "GET %s", oidc_cache_section_key(r->pool, section, key));

	if (reply == NULL)
		goto end;

	/* check that we got a string back */
	if (reply->type == REDIS_REPLY_NIL) {
		/* this is a normal cache miss, so we'll return OK */
		rv = TRUE;
		goto end;
	}

	if (reply->type != REDIS_REPLY_STRING) {
		oidc_error(r, "redisCommand reply type is not string: %d", reply->type);
		goto end;
	}

	/* do a sanity check on the returned value; report the two lengths but not the value
	 * itself, which is cached (session) data that must not be written to the log */
	if ((reply->str == NULL) || (reply->len != _oidc_strlen(reply->str))) {
		oidc_error(r, "redisCommand reply->len (%d) != _oidc_strlen(reply->str) (%d)", (int)reply->len,
			   (int)_oidc_strlen(reply->str));
		goto end;
	}

	/* copy it in to the request memory pool */
	*value = apr_pstrdup(r->pool, reply->str);

	/* return success */
	rv = TRUE;

end:

	/* free the reply object resources */
	oidc_cache_redis_reply_free(&reply);

	/* unlock the process mutex, unless the retry backoff released it and could not get it back */
	if ((context->request_scoped == FALSE) && (mutex_held == TRUE))
		oidc_cache_mutex_unlock(r->pool, r->server, context->mutex);

	/* return the status */
	return rv;
}

/*
 * store a name/value pair in Redis
 */
apr_byte_t oidc_cache_redis_set(request_rec *r, const char *section, const char *key, const char *value,
				apr_time_t expiry) {

	oidc_cfg_t *cfg = ap_get_module_config(r->server->module_config, &auth_openidc_module);
	oidc_cache_cfg_redis_t *context = (oidc_cache_cfg_redis_t *)cfg->cache.cfg;
	redisReply *reply = NULL;
	apr_byte_t rv = FALSE;
	apr_byte_t mutex_held = TRUE;
	apr_uint32_t timeout;

	/* grab the process lock, except in request-scoped connection mode where each request
	 * has its own connection and the mutex only guards the idle pool */
	if ((context->request_scoped == FALSE) && (oidc_cache_mutex_lock(r->pool, r->server, context->mutex) == FALSE))
		return FALSE;

	/* see if we should be clearing this entry */
	if (value == NULL) {

		/* delete it */
		reply = oidc_cache_redis_exec(r, context, &mutex_held, "DEL %s",
					      oidc_cache_section_key(r->pool, section, key));

	} else {

		/* calculate the timeout from now */
		timeout = (apr_uint32_t)apr_time_sec(expiry - apr_time_now());

		/* store it */
		reply = oidc_cache_redis_exec(r, context, &mutex_held, "SET %s %s EX %d",
					      oidc_cache_section_key(r->pool, section, key), value, timeout);
	}

	rv = (reply != NULL) && (reply->type != REDIS_REPLY_ERROR);

	/* free the reply object resources */
	oidc_cache_redis_reply_free(&reply);

	/* unlock the process mutex, unless the retry backoff released it and could not get it back */
	if ((context->request_scoped == FALSE) && (mutex_held == TRUE))
		oidc_cache_mutex_unlock(r->pool, r->server, context->mutex);

	/* return the status */
	return rv;
}

int oidc_cache_redis_destroy(apr_pool_t *pool, server_rec *s) {
	oidc_cfg_t *cfg = (oidc_cfg_t *)ap_get_module_config(s->module_config, &auth_openidc_module);
	oidc_cache_cfg_redis_t *context = (oidc_cache_cfg_redis_t *)cfg->cache.cfg;

	if (context != NULL) {
		oidc_cache_mutex_lock(pool, s, context->mutex);
		context->disconnect(context);
		/* release any idle pooled connections kept for request-scoped connection mode */
		for (int i = 0; i < context->idle_num; i++)
			redisFree(context->idle[i]);
		context->idle_num = 0;
		oidc_cache_mutex_unlock(pool, s, context->mutex);
		if (oidc_cache_mutex_destroy(s, context->mutex) != TRUE) {
			oidc_serror(s, "oidc_cache_mutex_destroy on refresh mutex failed");
		}
		cfg->cache.cfg = NULL;
	}

	return APR_SUCCESS;
}

// clang-format off

oidc_cache_t oidc_cache_redis = {
	"redis",
	1,
	oidc_cache_redis_post_config_hook,
	oidc_cache_redis_child_init,
	oidc_cache_redis_get,
	oidc_cache_redis_set,
	oidc_cache_redis_destroy
};

// clang-format on
