/* SPDX-License-Identifier: GPL-2.0-or-later */

/*
 * Implements the ACMEv2 RFC 8555 protocol
 */

#include "haproxy/ticks.h"
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>

#include <import/ebsttree.h>
#include <import/mjson.h>

#include <haproxy/acme-t.h>

#include <haproxy/acme_resolvers.h>
#include <haproxy/base64.h>
#include <haproxy/intops.h>
#include <haproxy/cfgparse.h>
#include <haproxy/cli.h>
#include <haproxy/errors.h>
#include <haproxy/http_client.h>
#include <haproxy/jws.h>
#include <haproxy/list.h>
#include <haproxy/log.h>
#include <haproxy/pattern.h>
#include <haproxy/resolvers.h>
#include <haproxy/sink.h>
#include <haproxy/ssl_ckch.h>
#include <haproxy/ssl_gencert.h>
#include <haproxy/ssl_sock.h>
#include <haproxy/ssl_utils.h>
#include <haproxy/tools.h>
#include <haproxy/trace.h>

#define TRACE_SOURCE &trace_acme

#if defined(HAVE_ACME)

static EVP_PKEY *tmp_pkey = NULL;
static X509 *tmp_x509 = NULL;


static void acme_trace(enum trace_level level, uint64_t mask, const struct trace_source *src,
                       const struct ist where, const struct ist func,
                       const void *a1, const void *a2, const void *a3, const void *a4);

static const struct trace_event acme_trace_events[] = {
	{ .mask = ACME_EV_SCHED,            .name = "acme_sched",            .desc = "Wakeup scheduled ACME task" },
	{ .mask = ACME_EV_NEW,              .name = "acme_new",              .desc = "New ACME task" },
	{ .mask = ACME_EV_TASK,             .name = "acme_task",             .desc = "ACME task" },
	{ }
};


static const struct name_desc acme_trace_lockon_args[4] = {
	/* arg1 */ { .name="acme_ctx", .desc="ACME context" },
	/* arg2 */ { },
	/* arg3 */ { },
	/* arg4 */ { }
};

static const struct name_desc acme_trace_decoding[] = {
	{ .name="clean",    .desc="only user-friendly stuff, generally suitable for level \"user\"" },
	{ .name="minimal",  .desc="report only conn, no real decoding" },
	{ .name="simple",   .desc="add error messages" },
	{ .name="advanced", .desc="add handshake-related details" },
	{ .name="complete", .desc="add full data dump when available" },
	{ /* end */ }
};


struct trace_source trace_acme = {
	.name = IST("acme"),
	.desc = "ACME",
	.arg_def = TRC_ARG_PRIV,
	.default_cb = acme_trace,
	.known_events = acme_trace_events,
	.lockon_args = acme_trace_lockon_args,
	.decoding = acme_trace_decoding,
	.report_events = ~0,  /* report everything by default */
};

INITCALL1(STG_REGISTER, trace_register_source, &trace_acme);

static void acme_trace(enum trace_level level, uint64_t mask, const struct trace_source *src,
                      const struct ist where, const struct ist func,
                      const void *a1, const void *a2, const void *a3, const void *a4)
{
	const struct acme_ctx *ctx = a1;

	if (src->verbosity <= ACME_VERB_CLEAN)
		return;

	chunk_appendf(&trace_buf, " :");

	if (mask >= ACME_EV_NEW)
		chunk_appendf(&trace_buf, " acme_ctx=%p", ctx);


	if (mask == ACME_EV_NEW)
		chunk_appendf(&trace_buf, ", crt=%s", ctx->store->path);

	if (mask >= ACME_EV_TASK) {

		switch (ctx->http_state) {
			case ACME_HTTP_REQ:
				chunk_appendf(&trace_buf, ", http_st: ACME_HTTP_REQ");
			break;
			case ACME_HTTP_RES:
				chunk_appendf(&trace_buf, ", http_st: ACME_HTTP_RES");
			break;
		}
		chunk_appendf(&trace_buf, ", st: ");
		switch (ctx->state) {
			case ACME_RESOURCES:         chunk_appendf(&trace_buf, "ACME_RESOURCES");        break;
			case ACME_NEWNONCE:          chunk_appendf(&trace_buf, "ACME_NEWNONCE");         break;
			case ACME_CHKACCOUNT:        chunk_appendf(&trace_buf, "ACME_CHKACCOUNT");       break;
			case ACME_NEWACCOUNT:        chunk_appendf(&trace_buf, "ACME_NEWACCOUNT");       break;
			case ACME_NEWORDER:          chunk_appendf(&trace_buf, "ACME_NEWORDER");         break;
			case ACME_AUTH:              chunk_appendf(&trace_buf, "ACME_AUTH");             break;
			case ACME_CLI_WAIT :         chunk_appendf(&trace_buf, "ACME_CLI_WAIT");         break;
			case ACME_INITIAL_DELAY:     chunk_appendf(&trace_buf, "ACME_INITIAL_DELAY");    break;
			case ACME_RSLV_RETRY_DELAY:  chunk_appendf(&trace_buf, "ACME_RSLV_RETRY_DELAY"); break;
			case ACME_RSLV_TRIGGER:      chunk_appendf(&trace_buf, "ACME_RSLV_TRIGGER");     break;
			case ACME_RSLV_READY:        chunk_appendf(&trace_buf, "ACME_RSLV_READY");       break;
			case ACME_CHALLENGE:         chunk_appendf(&trace_buf, "ACME_CHALLENGE");        break;
			case ACME_CHKCHALLENGE:      chunk_appendf(&trace_buf, "ACME_CHKCHALLENGE");     break;
			case ACME_FINALIZE:          chunk_appendf(&trace_buf, "ACME_FINALIZE");         break;
			case ACME_CHKORDER:          chunk_appendf(&trace_buf, "ACME_CHKORDER");         break;
			case ACME_CERTIFICATE:       chunk_appendf(&trace_buf, "ACME_CERTIFICATE");      break;
			case ACME_END:               chunk_appendf(&trace_buf, "ACME_END");              break;
		}
	}
	if (mask & (ACME_EV_REQ|ACME_EV_RES)) {
		const struct ist *url = a2;
		const struct buffer *buf = a3;

		if (mask & ACME_EV_REQ)
			chunk_appendf(&trace_buf, " url: %.*s", (int)url->len, url->ptr);

		if (src->verbosity >= ACME_VERB_COMPLETE && level >= TRACE_LEVEL_DATA) {
			chunk_appendf(&trace_buf, " Buffer Dump:\n");
			chunk_appendf(&trace_buf, "%.*s", (int)buf->data, buf->area);
		}
	}
}

struct eb_root acme_tasks = EB_ROOT_UNIQUE;
__decl_thread(HA_RWLOCK_T acme_lock);

static struct acme_cfg *acme_cfgs = NULL;
static struct acme_cfg *cur_acme = NULL;

static struct proxy *httpclient_acme_px = NULL;

enum acme_ret {
	ACME_RET_OK = 0,
	ACME_RET_RETRY = 1,
	ACME_RET_FAIL = 2
};

static int acme_start_task(struct ckch_store *store, char **errmsg);
static struct task *acme_scheduler(struct task *task, void *context, unsigned int state);

/* Return an existing acme_cfg section */
struct acme_cfg *get_acme_cfg(const char *name)
{
	struct acme_cfg *tmp_acme = acme_cfgs;

	/* first check if the ID was already used */
	while (tmp_acme) {
		if (strcmp(tmp_acme->name, name) == 0)
			return tmp_acme;

		tmp_acme = tmp_acme->next;
	}
	return NULL;
}

/* Return an existing section section OR create one and return it */
struct acme_cfg *new_acme_cfg(const char *name)
{
	struct acme_cfg *ret = NULL;

	/* first check if the ID was already used. return it if that's the case */
	if ((ret = get_acme_cfg(name)) != NULL)
		goto out;

	/* If there wasn't any section with this name, just create one */
	ret = calloc(1, sizeof(*ret));
	if (!ret)
		return NULL;

	ret->name = strdup(name);
	/* 0 on the linenum just mean it was not initialized yet */
	ret->linenum = 0;

	ret->challenge = strdup("http-01"); /* default value */
	ret->dns_delay = 30; /* default DNS re-trigger delay in seconds */
	ret->dns_timeout = 600; /* default DNS retry timeout */

	/* The default generated keys are EC-384 */
	ret->key.type = EVP_PKEY_EC;
	ret->key.curves = NID_secp384r1;

	/* default to 2048 bits when using RSA */
	ret->key.bits = 2048;

	ret->next = acme_cfgs;
	acme_cfgs = ret;

out:
	return ret;
}

/*
 * ckch_conf acme parser
 */
int ckch_conf_acme_init(void *value, char *buf, struct ckch_store *s, int cli, const char *filename, int linenum, char **err)
{
	int err_code = 0;
	struct acme_cfg *cfg;

	cfg = new_acme_cfg(value);
	if (!cfg) {
		memprintf(err, "out of memory.\n");
		err_code |= ERR_FATAL| ERR_ALERT;
		goto error;
	}

	if (cfg->linenum == 0) {
		if (filename)
			cfg->filename = strdup(filename);
                /* store the linenum as a negative value because is the one of
                 * the crt-store, not the one of the section. It will be replace
                 * by the one of the section once initialized
                 */
                cfg->linenum = -linenum;
	}

error:
	return err_code;
}

/* Initialize the proxy for the ACME HTTP client */
static int httpclient_acme_init()
{
	httpclient_acme_px = httpclient_create_proxy("<ACME>");
	if (!httpclient_acme_px)
		return ERR_FATAL;
	httpclient_acme_px->logformat.str = httpsclient_log_format; /* ACME server are always SSL */

	return ERR_NONE;
}


/* acme section parser
 * Fill the acme_cfgs linked list
 */
static int cfg_parse_acme(const char *file, int linenum, char **args, int kwm)
{
	struct cfg_kw_list *kwl;
	const char *best;
	int index;
	int rc = 0;
	int err_code = 0;
	char *errmsg = NULL;

	if (!experimental_directives_allowed) {
		ha_alert("parsing [%s:%d]: section '%s' is experimental, must be allowed via a global 'expose-experimental-directives'\n", file, linenum, cursection);
		err_code |= ERR_ALERT | ERR_FATAL;
		goto out;
	}
	mark_tainted(TAINTED_CONFIG_EXP_KW_DECLARED);

	if (strcmp(args[0], "acme") == 0) {

		if (alertif_too_many_args(1, file, linenum, args, &err_code))
			goto out;

		if (!*args[1]) {
			err_code |= ERR_ALERT | ERR_FATAL;
			ha_alert("parsing [%s:%d]: section '%s' requires an ID argument.\n", file, linenum, cursection);
			goto out;
		}

		cur_acme = new_acme_cfg(args[1]);
		if (!cur_acme) {
			err_code |= ERR_ALERT | ERR_FATAL;
			ha_alert("parsing [%s:%d]: out of memory.\n", file, linenum);
			goto out;
		}


		/* first check if the ID was already used */
		if (cur_acme->linenum > 0) {
			/* an uninitialized section is created when parsing the "acme" keyword in a crt-store, with a
			 * linenum <= 0, however, when the linenum > 0, it means we already created a section with this
			 * name */
			err_code |= ERR_ALERT | ERR_FATAL;
			ha_alert("parsing [%s:%d]: acme section '%s' already exists (%s:%d).\n",
			         file, linenum, args[1], cur_acme->filename, cur_acme->linenum);
			goto out;
		}

		ha_free(&cur_acme->filename);
		cur_acme->filename = strdup(file);
		cur_acme->linenum = linenum;

		goto out;
	}

	list_for_each_entry(kwl, &cfg_keywords.list, list) {
		for (index = 0; kwl->kw[index].kw != NULL; index++) {
			if (kwl->kw[index].section != CFG_ACME)
				continue;
			if (strcmp(kwl->kw[index].kw, args[0]) == 0) {
				if (check_kw_experimental(&kwl->kw[index], file, linenum, &errmsg)) {
					ha_alert("%s\n", errmsg);
					err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
					goto out;
				}

				/* prepare error message just in case */
				rc = kwl->kw[index].parse(args, CFG_ACME, NULL, NULL, file, linenum, &errmsg);
				if (rc & ERR_ALERT) {
					ha_alert("parsing [%s:%d] : %s\n", file, linenum, errmsg);
					err_code |= rc;
					goto out;
				}
				else if (rc & ERR_WARN) {
					ha_warning("parsing [%s:%d] : %s\n", file, linenum, errmsg);
					err_code |= rc;
					goto out;
				}
				goto out;
			}
		}
	}

	best = cfg_find_best_match(args[0], &cfg_keywords.list, CFG_ACME, NULL);
	if (best)
		ha_alert("parsing [%s:%d] : unknown keyword '%s' in '%s' section; did you mean '%s' maybe ?\n", file, linenum, args[0], cursection, best);
	else
		ha_alert("parsing [%s:%d] : unknown keyword '%s' in '%s' section\n", file, linenum, args[0], cursection);
	err_code |= ERR_ALERT | ERR_FATAL;
	goto out;

out:
	if (err_code & ERR_FATAL)
		err_code |= ERR_ABORT;
	free(errmsg);
	return err_code;


}

static int cfg_parse_acme_kws(char **args, int section_type, struct proxy *curpx, const struct proxy *defpx,
                              const char *file, int linenum, char **err)
{
	int err_code = 0;
	char *errmsg = NULL;

	if (strcmp(args[0], "directory") == 0) {
		if (!*args[1]) {
			ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires an argument\n", file, linenum, args[0], cursection);
			err_code |= ERR_ALERT | ERR_FATAL;
			goto out;
		}
		if (alertif_too_many_args(1, file, linenum, args, &err_code))
			goto out;
		ha_free(&cur_acme->directory);
		cur_acme->directory = strdup(args[1]);
		if (!cur_acme->directory) {
			err_code |= ERR_ALERT | ERR_FATAL;
			ha_alert("parsing [%s:%d]: out of memory.\n", file, linenum);
			goto out;
		}
	} else if (strcmp(args[0], "contact") == 0) {
		/* save the contact email */
		if (!*args[1]) {
			ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires an argument\n", file, linenum, args[0], cursection);
			err_code |= ERR_ALERT | ERR_FATAL;
			goto out;
		}
		if (alertif_too_many_args(1, file, linenum, args, &err_code))
			goto out;

		ha_free(&cur_acme->account.contact);
		cur_acme->account.contact = strdup(args[1]);
		if (!cur_acme->account.contact) {
			err_code |= ERR_ALERT | ERR_FATAL;
			ha_alert("parsing [%s:%d]: out of memory.\n", file, linenum);
			goto out;
		}
	} else if (strcmp(args[0], "account-key") == 0) {
		/* save the filename of the account key */
		if (!*args[1]) {
			ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires a filename argument\n", file, linenum, args[0], cursection);
			err_code |= ERR_ALERT | ERR_FATAL;
			goto out;
		}
		if (alertif_too_many_args(1, file, linenum, args, &err_code))
			goto out;

		ha_free(&cur_acme->account.file);
		cur_acme->account.file = strdup(args[1]);
		if (!cur_acme->account.file) {
			err_code |= ERR_ALERT | ERR_FATAL;
			ha_alert("parsing [%s:%d]: out of memory.\n", file, linenum);
			goto out;
		}
	} else if (strcmp(args[0], "challenge") == 0) {
		if ((!*args[1]) ||  (strcasecmp("http-01", args[1]) != 0 && (strcasecmp("dns-01", args[1]) != 0))) {
			ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires a challenge type: http-01 or dns-01\n", file, linenum, args[0], cursection);
			err_code |= ERR_ALERT | ERR_FATAL;
			goto out;
		}

		if (alertif_too_many_args(1, file, linenum, args, &err_code))
			goto out;

		ha_free(&cur_acme->challenge);
		cur_acme->challenge = strdup(args[1]);
		if (!cur_acme->challenge) {
			err_code |= ERR_ALERT | ERR_FATAL;
			ha_alert("parsing [%s:%d]: out of memory.\n", file, linenum);
			goto out;
		}

		/* require the CLI by default */
		if ((strcasecmp("dns-01", args[1]) == 0) && (cur_acme->cond_ready == 0)) {
			cur_acme->cond_ready = ACME_RDY_CLI;
		}

		if ((strcasecmp("http-01", args[1]) == 0) && (cur_acme->cond_ready != 0)) {
			ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section, \"http-01\" is not compatible with the \"challenge-ready\" option\n", file, linenum, args[0], cursection);
			err_code |= ERR_ALERT | ERR_FATAL;
			goto out;
		}

	} else if (strcmp(args[0], "map") == 0) {
		/* save the map name for thumbprint + token storage */
		if (!*args[1]) {
			ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires an argument\n", file, linenum, args[0], cursection);
			err_code |= ERR_ALERT | ERR_FATAL;
			goto out;
		}
		if (alertif_too_many_args(1, file, linenum, args, &err_code))
			goto out;

		ha_free(&cur_acme->map);
		cur_acme->map = strdup(args[1]);
		if (!cur_acme->map) {
			err_code |= ERR_ALERT | ERR_FATAL;
			ha_alert("parsing [%s:%d]: out of memory.\n", file, linenum);
			goto out;
		}
	} else if (strcmp(args[0], "challenge-ready") == 0) {
		char *str = args[1];
		char *saveptr;

		if (!*args[1]) {
			ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires an argument\n", file, linenum, args[0], cursection);
			err_code |= ERR_ALERT | ERR_FATAL;
			goto out;
		}
		if (alertif_too_many_args(1, file, linenum, args, &err_code))
			goto out;

		cur_acme->cond_ready = 0;

		while ((str = strtok_r(str, ",", &saveptr))) {

			if (strcmp(str, "cli") == 0) {
				/* wait for the CLI-ready to run the challenge */
				cur_acme->cond_ready |= ACME_RDY_CLI;
			} else if (strcmp(str, "dns") == 0) {
				/* wait for the DNS-check to run the challenge */
				cur_acme->cond_ready |= ACME_RDY_DNS;
			} else if (strcmp(str, "delay") == 0) {
				/* wait for the DNS-check to run the challenge */
				cur_acme->cond_ready |= ACME_RDY_DELAY;
			} else if (strcmp(str, "none") == 0) {
				if (cur_acme->cond_ready || (saveptr && *saveptr)) {
					err_code |= ERR_ALERT | ERR_FATAL;
					ha_alert("parsing [%s:%d]: keyword '%s' in '%s' can't combine 'none' with other keywords.\n", file, linenum, args[0], cursection);
					goto out;
				}
				cur_acme->cond_ready = ACME_RDY_NONE;
			} else {
				err_code |= ERR_ALERT | ERR_FATAL;
				ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires parameter separated by commas: 'cli', 'dns' or 'none'\n", file, linenum, args[0], cursection);
				goto out;
			}
			str = NULL;
		}

		if ((strcasecmp("http-01", cur_acme->challenge) == 0) && (cur_acme->cond_ready != 0)) {
			ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section, \"http-01\" is not compatible with the \"challenge-ready\" option\n", file, linenum, args[0], cursection);
			err_code |= ERR_ALERT | ERR_FATAL;
			goto out;
		}

	} else if (strcmp(args[0], "dns-delay") == 0) {
		const char *res;

		if (!*args[1]) {
			ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires an argument\n", file, linenum, args[0], cursection);
			err_code |= ERR_ALERT | ERR_FATAL;
			goto out;
		}
		if (alertif_too_many_args(1, file, linenum, args, &err_code))
			goto out;

		res = parse_time_err(args[1], &cur_acme->dns_delay, TIME_UNIT_S);
		if (res == PARSE_TIME_OVER) {
			ha_alert("parsing [%s:%d]: timer overflow in argument <%s> to '%s'\n", file, linenum, args[1], args[0]);
			err_code |= ERR_ALERT | ERR_FATAL;
			goto out;
		} else if (res == PARSE_TIME_UNDER) {
			ha_alert("parsing [%s:%d]: timer underflow in argument <%s> to '%s'\n", file, linenum, args[1], args[0]);
			err_code |= ERR_ALERT | ERR_FATAL;
			goto out;
		} else if (res) {
			ha_alert("parsing [%s:%d]: unexpected character '%c' in argument to '%s'\n", file, linenum, *res, args[0]);
			err_code |= ERR_ALERT | ERR_FATAL;
			goto out;
		}
	} else if (strcmp(args[0], "dns-timeout") == 0) {
		const char *res;

		if (!*args[1]) {
			ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires an argument\n", file, linenum, args[0], cursection);
			err_code |= ERR_ALERT | ERR_FATAL;
			goto out;
		}
		if (alertif_too_many_args(1, file, linenum, args, &err_code))
			goto out;

		res = parse_time_err(args[1], &cur_acme->dns_timeout, TIME_UNIT_S);
		if (res == PARSE_TIME_OVER) {
			ha_alert("parsing [%s:%d]: timer overflow in argument <%s> to '%s'\n", file, linenum, args[1], args[0]);
			err_code |= ERR_ALERT | ERR_FATAL;
			goto out;
		} else if (res == PARSE_TIME_UNDER) {
			ha_alert("parsing [%s:%d]: timer underflow in argument <%s> to '%s'\n", file, linenum, args[1], args[0]);
			err_code |= ERR_ALERT | ERR_FATAL;
			goto out;
		} else if (res) {
			ha_alert("parsing [%s:%d]: unexpected character '%c' in argument to '%s'\n", file, linenum, *res, args[0]);
			err_code |= ERR_ALERT | ERR_FATAL;
			goto out;
		}
	} else if (strcmp(args[0], "reuse-key") == 0) {
		if (!*args[1]) {
			ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires an argument\n", file, linenum, args[0], cursection);
			err_code |= ERR_ALERT | ERR_FATAL;
			goto out;
		}
		if (alertif_too_many_args(1, file, linenum, args, &err_code))
			goto out;

		if (strcmp(args[1], "on") == 0) {
			cur_acme->reuse_key = 1;
		} else if (strcmp(args[1], "off") == 0) {
			cur_acme->reuse_key = 0;
		} else {
			err_code |= ERR_ALERT | ERR_FATAL;
			ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires either the 'on' or 'off' parameter\n", file, linenum, args[0], cursection);
			goto out;
		}
	} else if (*args[0] != 0) {
		ha_alert("parsing [%s:%d]: unknown keyword '%s' in '%s' section\n", file, linenum, args[0], cursection);
		err_code |= ERR_ALERT | ERR_FATAL;
		goto out;
	}
out:
	free(errmsg);
	return err_code;
}


/* parsing "acme-provider" and "acme-vars" and add escaping of double quotes */
static int cfg_parse_acme_vars_provider(char **args, int section_type, struct proxy *curpx, const struct proxy *defpx,
                                        const char *file, int linenum, char **err)
{
	int err_code = 0;
	char *errmsg = NULL;
	char **dst = NULL;
	char *src = args[1];
	char *tmp = NULL;
	int i = 0;
	int len;

	if (strcmp(args[0], "acme-vars") == 0) {
		dst = &cur_acme->vars;
	} else if (strcmp(args[0], "provider-name") == 0) {
		dst = &cur_acme->provider;
	} else {
		err_code |= ERR_ALERT | ERR_FATAL;
		ha_alert("parsing [%s:%d]: unsupported keyword '%s'.\n", file, linenum, args[0]);
		goto out;
	}

	if (dst)
		free(*dst);

	if (!*args[1]) {
		ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires an argument\n", file, linenum, args[0], cursection);
		err_code |= ERR_ALERT | ERR_FATAL;
		goto out;
	}
	if (alertif_too_many_args(1, file, linenum, args, &err_code))
		goto out;

	len = strlen(src);
	tmp = malloc(len + 1);
	if (!tmp)
		goto vars_end;

	/* escape the " character */
	while (*src) {
		if (*src == '"') {
			char *tmp2 = NULL;

			len++;
			tmp2 = realloc(tmp, len + 1);
			if (!tmp2) {
				ha_free(&tmp);
				goto vars_end;
			}
			tmp = tmp2;
			tmp[i++] = '\\'; /* add escaping */
		}
		tmp[i++] = *src;
		src++;
	}
	tmp[i] = '\0';

vars_end:
	*dst = tmp;
	if (!*dst) {
		err_code |= ERR_ALERT | ERR_FATAL;
		ha_alert("parsing [%s:%d]: out of memory.\n", file, linenum);
		goto out;
	}

out:
	free(errmsg);
	return err_code;
}

static int cfg_parse_acme_cfg_key(char **args, int section_type, struct proxy *curpx, const struct proxy *defpx,
                              const char *file, int linenum, char **err)
{
	int err_code = 0;
	char *errmsg = NULL;

	if (strcmp(args[0], "keytype") == 0) {
		if (!*args[1]) {
			ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires an argument\n", file, linenum, args[0], cursection);
			err_code |= ERR_ALERT | ERR_FATAL;
			goto out;
		}
		if (alertif_too_many_args(1, file, linenum, args, &err_code))
			goto out;

		if (strcmp(args[1], "RSA") == 0) {
			cur_acme->key.type = EVP_PKEY_RSA;
		} else if (strcmp(args[1], "ECDSA") == 0) {
			cur_acme->key.type = EVP_PKEY_EC;
		} else {
			ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires either 'RSA' or 'ECDSA' argument\n", file, linenum, args[0], cursection);
			err_code |= ERR_ALERT | ERR_FATAL;
			goto out;
		}

	} else if (strcmp(args[0], "bits") == 0) {
		char *stop;

		if (!*args[1]) {
			ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires an argument\n", file, linenum, args[0], cursection);
			err_code |= ERR_ALERT | ERR_FATAL;
			goto out;
		}

		cur_acme->key.bits = strtol(args[1], &stop, 10);
		if (*stop != '\0') {
			err_code |= ERR_ALERT | ERR_FATAL;
			ha_alert("parsing [%s:%d] : cannot parse '%s' value '%s', an integer is expected.\n", file, linenum, args[0], args[1]);
			goto out;
		}

		if (alertif_too_many_args(1, file, linenum, args, &err_code))
			goto out;

	} else if (strcmp(args[0], "curves") == 0) {
		if (!*args[1]) {
			ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires an argument\n", file, linenum, args[0], cursection);
			err_code |= ERR_ALERT | ERR_FATAL;
			goto out;
		}
		if (alertif_too_many_args(1, file, linenum, args, &err_code))
			goto out;

		if ((cur_acme->key.curves = curves2nid(args[1])) == -1) {
			ha_alert("parsing [%s:%d]: unsupported curves '%s'\n", file, linenum, args[1]);
			err_code |= ERR_ALERT | ERR_FATAL;
			goto out;
		}
	}

out:
	free(errmsg);
	return err_code;
}

/* parse 'acme.scheduler' option */
static int cfg_parse_global_acme_sched(char **args, int section_type, struct proxy *curpx, const struct proxy *defpx,
                                       const char *file, int linenum, char **err)
{
	int err_code = 0;

	if (!*args[1]) {
		memprintf(err, "parsing [%s:%d]: keyword '%s' in '%s' section requires an argument\n", file, linenum, args[0], cursection);
		goto error;
	}
	if (alertif_too_many_args(1, file, linenum, args, &err_code))
		goto error;

	if (strcmp(args[1], "auto") == 0) {
		global_ssl.acme_scheduler = 1;
	} else if (strcmp(args[1], "off") == 0) {
		global_ssl.acme_scheduler = 0;
	} else {
		memprintf(err, "parsing [%s:%d]: keyword '%s' in '%s' section requires either 'auto' or 'off' argument", file, linenum, args[0], cursection);
		goto error;
	}

	return 0;

error:
	return -1;
}

/* Initialize stuff once the section is parsed */
static int cfg_postsection_acme()
{
	struct ckch_store *store;
	EVP_PKEY *key = NULL;
	BIO *bio = NULL;
	int err_code = 0;
	char *errmsg = NULL;
	char *path;
	char store_path[PATH_MAX]; /* complete path with crt_base */
	struct stat st;

	/* if account key filename is unspecified, choose a filename for it */
	if (!cur_acme->account.file) {
		if (!memprintf(&cur_acme->account.file, "%s.account.key", cur_acme->name)) {
			err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
			ha_alert("acme: out of memory.\n");
			goto out;
		}
	}


	if (global_ssl.crt_base && *cur_acme->account.file != '/') {
		int rv;
		/* When no crt_store name, complete the name in the ckch_tree with 'crt-base' */

		rv = snprintf(store_path, sizeof(store_path), "%s/%s", global_ssl.crt_base, cur_acme->account.file);
		if (rv >= sizeof(store_path)) {
			ha_alert("'%s/%s' : path too long", global_ssl.crt_base, cur_acme->account.file);
			err_code |= ERR_ALERT | ERR_FATAL;
			goto out;
		}
		path = store_path;
	} else {
		path = cur_acme->account.file;
	}

	if (!cur_acme->directory) {
		err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
		ha_alert("acme: No directory defined in ACME section '%s'.\n", cur_acme->name);
		goto out;
	}

	store = ckch_store_new(path);
	if (!store) {
		ha_alert("acme: out of memory.\n");
		err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
		goto out;
	}

	/* tries to open the account key  */
	if (stat(path, &st) == 0) {
		if (ssl_sock_load_key_into_ckch(path, NULL, store->data, &errmsg)) {
			memprintf(&errmsg, "%s'%s' is present but cannot be read or parsed.\n", errmsg && *errmsg ? errmsg : NULL, path);
			if (errmsg && *errmsg)
				indent_msg(&errmsg, 8);
			err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
			ha_alert("acme: %s\n", errmsg);
			goto out;
		}
		/* ha_notice("acme: reading account key '%s' for id '%s'.\n", path, cur_acme->name); */
	} else {
		ha_notice("acme: generate account key '%s' for acme section '%s'.\n", path, cur_acme->name);

		if ((key = ssl_gen_EVP_PKEY(cur_acme->key.type, cur_acme->key.curves, cur_acme->key.bits, &errmsg)) == NULL) {
			ha_alert("acme: %s\n", errmsg);
			goto out;
		}

		if ((bio = BIO_new_file(store->path, "w+")) == NULL) {
			ha_alert("acme: cannot create the file '%s', check your permissions.\n", cur_acme->account.file);
			err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
			goto out;
		}

		if ((PEM_write_bio_PrivateKey(bio, key, NULL, NULL, 0, NULL, NULL)) == 0) {
			ha_alert("acme: cannot write account key '%s'.\n", cur_acme->account.file);
			err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
			goto out;
		}

		store->data->key = key;
		key = NULL;
	}

	if (store->data->key == NULL) {
		ha_alert("acme: No Private Key found in '%s'.\n", path);
		err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
		goto out;
	}

	cur_acme->account.pkey = store->data->key;
	EVP_PKEY_up_ref(cur_acme->account.pkey);

	trash.data = jws_thumbprint(cur_acme->account.pkey, trash.area, trash.size);

	cur_acme->account.thumbprint = my_strndup(trash.area, trash.data);
	if (!cur_acme->account.thumbprint) {
		ha_alert("acme: out of memory.\n");
		err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
		goto out;
	}

	ckch_store_free(store);

out:
	EVP_PKEY_free(key);
	BIO_free_all(bio);
	ha_free(&errmsg);
	return err_code;
}

/* initialize the httpclient just before check_config_validity() because it could create a defaults resolver if it
 * doesn't exist. */
static int cfg_precheck_acme()
{
	if (acme_cfgs) {
		if (httpclient_acme_init() & ERR_FATAL) {
			ha_alert("couldn't initialize the httpclient for ACME.\n");
			return ERR_ABORT;

		}
	}
	return ERR_NONE;
}
REGISTER_PRE_CHECK(cfg_precheck_acme);

/* postparser function checks if the ACME section was declared */
static int cfg_postparser_acme()
{
	struct acme_cfg *tmp_acme = acme_cfgs;
	struct task *task = NULL;
	int ret = 0;

        /* first check if the ID was already used */
	while (tmp_acme) {
		/* if the linenum is not > 0, it means the acme keyword was used without declaring a section, and the
		 * linenum of the crt-store is stored negatively */
		if (tmp_acme->linenum <= 0) {
			ret++;
			ha_alert("acme '%s' was used on a crt line [%s:%d], but no '%s' section exists!\n",
			         tmp_acme->name, tmp_acme->filename, -tmp_acme->linenum, tmp_acme->name);
		}
		if (tmp_acme->map) {
			struct pat_ref *ref;

			ref = pat_ref_lookup(tmp_acme->map);
			if (!ref) {
				ret++;
				ha_alert("acme section '%s' line [%s:%d] has the map '%s' configured, but this map doesn't exist\n",
				         tmp_acme->name, tmp_acme->filename, tmp_acme->linenum, tmp_acme->map);
			}
		}
		tmp_acme = tmp_acme->next;
	}


	if (acme_cfgs && global_ssl.acme_scheduler) {
		task = task_new_anywhere();
		if (!task) {
			ret++;
			ha_alert("acme: couldn't start the scheduler!\n");
			goto end;
		}
		task->nice = 0;
		task->process = acme_scheduler;

		task_wakeup(task, TASK_WOKEN_INIT);
	}

end:
	return ret;
}

REGISTER_CONFIG_POSTPARSER("acme", cfg_postparser_acme);

void deinit_acme()
{
	struct acme_cfg *next = NULL;

	while (acme_cfgs) {

		next = acme_cfgs->next;
		ha_free(&acme_cfgs->filename);
		ha_free(&acme_cfgs->name);
		ha_free(&acme_cfgs->directory);
		ha_free(&acme_cfgs->account.contact);
		ha_free(&acme_cfgs->account.file);
		ha_free(&acme_cfgs->account.thumbprint);
		EVP_PKEY_free(acme_cfgs->account.pkey);
		ha_free(&acme_cfgs->vars);
		ha_free(&acme_cfgs->provider);
		ha_free(&acme_cfgs->challenge);
		ha_free(&acme_cfgs->map);

		free(acme_cfgs);
		acme_cfgs = next;
	}
}

REGISTER_POST_DEINIT(deinit_acme);

static struct cfg_kw_list cfg_kws_acme = {ILH, {
	{ CFG_ACME, "directory",  cfg_parse_acme_kws },
	{ CFG_ACME, "contact",  cfg_parse_acme_kws },
	{ CFG_ACME, "account-key",  cfg_parse_acme_kws },
	{ CFG_ACME, "challenge",  cfg_parse_acme_kws },
	{ CFG_ACME, "keytype",  cfg_parse_acme_cfg_key },
	{ CFG_ACME, "bits",  cfg_parse_acme_cfg_key },
	{ CFG_ACME, "curves",  cfg_parse_acme_cfg_key },
	{ CFG_ACME, "map",  cfg_parse_acme_kws },
	{ CFG_ACME, "reuse-key",  cfg_parse_acme_kws },
	{ CFG_ACME, "challenge-ready",  cfg_parse_acme_kws },
	{ CFG_ACME, "dns-delay",  cfg_parse_acme_kws },
	{ CFG_ACME, "dns-timeout",  cfg_parse_acme_kws },
	{ CFG_ACME, "acme-vars",  cfg_parse_acme_vars_provider },
	{ CFG_ACME, "provider-name",  cfg_parse_acme_vars_provider },
	{ CFG_GLOBAL, "acme.scheduler", cfg_parse_global_acme_sched },
	{ 0, NULL, NULL },
}};

INITCALL1(STG_REGISTER, cfg_register_keywords, &cfg_kws_acme);

REGISTER_CONFIG_SECTION("acme", cfg_parse_acme, cfg_postsection_acme);


/* free acme_ctx and its content
 *
 * Only acme_cfg and the httpclient is not free
 *
 */
static void acme_ctx_destroy(struct acme_ctx *ctx)
{
	struct acme_auth *auth;

	if (!ctx)
		return;

	istfree(&ctx->resources.newNonce);
	istfree(&ctx->resources.newAccount);
	istfree(&ctx->resources.newOrder);
	istfree(&ctx->nonce);
	istfree(&ctx->kid);
	istfree(&ctx->order);

	auth = ctx->auths;
	while (auth) {
		struct acme_auth *next;

		istfree(&auth->auth);
		istfree(&auth->chall);
		istfree(&auth->token);
		istfree(&auth->dns);
		acme_rslv_free(auth->rslv);
		next = auth->next;
		free(auth);
		auth = next;
	}

	istfree(&ctx->finalize);
	istfree(&ctx->certificate);

	ckch_store_free(ctx->store);

	X509_REQ_free(ctx->req);


	free(ctx);
}

static void acme_httpclient_end(struct httpclient *hc)
{
	struct task *task = hc->caller;
	struct acme_ctx *ctx;

	if (!task)
		return;

	ctx = task->context;

	if (ctx->http_state == ACME_HTTP_REQ)
		ctx->http_state = ACME_HTTP_RES;

	task_wakeup(task, TASK_WOKEN_MSG);
}

/*
 * Add a map entry with <challenge> as the key, and <thumprint> as value in the <map>.
 * Return 0 upon success or 1 otherwise.
 */
static int acme_add_challenge_map(const char *map, const char *challenge, const char *thumbprint, char **errmsg)
{
	int ret = 1;
	struct pat_ref *ref;
	struct pat_ref_elt *elt;

	/* when no map configured, return without error */
	if (!map)
		return 0;

	ref = pat_ref_lookup(map);
	if (!ref) {
		memprintf(errmsg, "Unknown map identifier '%s'.\n", map);
		goto out;
	}

	HA_RWLOCK_WRLOCK(PATREF_LOCK, &ref->lock);
	elt = pat_ref_load(ref, ref->curr_gen, challenge, thumbprint, -1, errmsg);
	HA_RWLOCK_WRUNLOCK(PATREF_LOCK, &ref->lock);

	if (elt == NULL)
		goto out;

	ret = 0;

out:
	return ret;
}

/*
 * Remove the <challenge> from the <map>
 */
static void acme_del_challenge_map(const char *map, const char *challenge)
{
	struct pat_ref *ref;

	/* when no map configured, return without error */
	if (!map)
		return;

	ref = pat_ref_lookup(map);
	if (!ref)
		goto out;

	HA_RWLOCK_WRLOCK(PATREF_LOCK, &ref->lock);
	pat_ref_delete(ref, challenge);
	HA_RWLOCK_WRUNLOCK(PATREF_LOCK, &ref->lock);

out:
	return;
}

/*
 * Remove all challenges from an acme_ctx from the <map>
 */
static void acme_del_acme_ctx_map(const struct acme_ctx *ctx)
{
	struct acme_auth *auth;

	/* when no map configured, return without error */
	if (!ctx->cfg->map)
		return;

	auth = ctx->auths;
	while (auth) {
		acme_del_challenge_map(ctx->cfg->map, auth->token.ptr);
		auth = auth->next;
	}
	return;
}

int acme_http_req(struct task *task, struct acme_ctx *ctx, struct ist url, enum http_meth_t meth, const struct http_hdr *hdrs, struct ist payload)
{
	struct httpclient *hc;

	hc = httpclient_new_from_proxy(httpclient_acme_px, task, meth, url);
	if (!hc)
		goto error;

	if (httpclient_req_gen(hc, hc->req.url, hc->req.meth, hdrs, payload) != ERR_NONE)
		goto error;

	hc->ops.res_end = acme_httpclient_end;

	ctx->hc = hc;

	if (!httpclient_start(hc))
		goto error;

	return 0;
error:
	httpclient_destroy(hc);
	ctx->hc = NULL;

	return 1;

}

/*
 * compute a TXT record for dns-01 challenge
 *  base64url(sha256(token || '.' || base64url(Thumbprint(accountKey))))
 *
 *  https://datatracker.ietf.org/doc/html/rfc8555/#section-8.4
 *
 */
unsigned int acme_txt_record(const struct ist thumbprint, const struct ist token, struct buffer *output)
{
	unsigned char md[EVP_MAX_MD_SIZE];
	struct buffer *tmp = NULL;
	unsigned int size;
	int ret = 0;


	if ((tmp = alloc_trash_chunk()) == NULL)
		goto out;

	chunk_istcat(tmp, token);
	chunk_appendf(tmp, ".");
	chunk_istcat(tmp, thumbprint);

	if (EVP_Digest(tmp->area, tmp->data, md, &size, EVP_sha256(), NULL) == 0)
		goto out;

	ret = a2base64url((const char *)md, size, output->area, output->size);
	if (ret < 0)
		ret = 0;
	output->data = ret;

out:
	free_trash_chunk(tmp);

	return ret;
}


int acme_jws_payload(struct buffer *req, struct ist nonce, struct ist url, EVP_PKEY *pkey, struct ist kid, struct buffer *output, char **errmsg)
{
	struct buffer *b64payload = NULL;
	struct buffer *b64prot = NULL;
	struct buffer *b64sign = NULL;
	struct buffer *jwk = NULL;
	enum jwt_alg alg = JWS_ALG_NONE;
	int ret = 1;


	b64payload = alloc_trash_chunk();
	b64prot = alloc_trash_chunk();
	jwk = alloc_trash_chunk();
	b64sign = alloc_trash_chunk();

	if (!b64payload || !b64prot || !jwk || !b64sign || !output) {
		memprintf(errmsg, "out of memory");
		goto error;
	}

	if (!isttest(kid))
		jwk->data = EVP_PKEY_to_pub_jwk(pkey, jwk->area, jwk->size);
	alg = EVP_PKEY_to_jws_alg(pkey);

	if (alg == JWS_ALG_NONE) {
		memprintf(errmsg, "couldn't chose a JWK algorithm");
		goto error;
	}

	b64payload->data = jws_b64_payload(req->area, b64payload->area, b64payload->size);
	b64prot->data = jws_b64_protected(alg, kid.ptr, jwk->area, nonce.ptr, url.ptr, b64prot->area, b64prot->size);
	b64sign->data = jws_b64_signature(pkey, alg, b64prot->area, b64payload->area, b64sign->area, b64sign->size);
	output->data = jws_flattened(b64prot->area, b64payload->area, b64sign->area, output->area, output->size);

	if (output->data == 0)
		goto error;

	ret = 0;

error:
	free_trash_chunk(b64sign);
	free_trash_chunk(jwk);
	free_trash_chunk(b64prot);
	free_trash_chunk(b64payload);


	return ret;
}

/*
 * Update every certificate instances for the new store
 *
 * XXX: ideally this should be reentrant like in lua or the CLI.
 */
int acme_update_certificate(struct task *task, struct acme_ctx *ctx, char **errmsg)
{
	int ret = 1;
	struct ckch_store *old_ckchs, *new_ckchs;
	struct ckch_inst *ckchi;
	struct sink *dpapi;
	struct ist line[3];

	new_ckchs = ctx->store;

	if (HA_SPIN_TRYLOCK(CKCH_LOCK, &ckch_lock)) {
		memprintf(errmsg, "couldn't get the certificate lock!");
		return ret;
	}

	if ((old_ckchs = ckchs_lookup(new_ckchs->path)) == NULL) {
		memprintf(errmsg, "couldn't find the previous certificate to update");
		goto error;
	}

	ckchi = LIST_ELEM(old_ckchs->ckch_inst.n, typeof(ckchi), by_ckchs);

	/* walk through the old ckch_inst and creates new ckch_inst using the updated ckchs */
	list_for_each_entry_from(ckchi, &old_ckchs->ckch_inst, by_ckchs) {
		struct ckch_inst *new_inst;

		if (ckch_inst_rebuild(new_ckchs, ckchi, &new_inst, errmsg)) {
			goto error;
		}

		/* link the new ckch_inst to the duplicate */
		LIST_APPEND(&new_ckchs->ckch_inst, &new_inst->by_ckchs);
	}

	/* insert everything and remove the previous objects */
	ckch_store_replace(old_ckchs, new_ckchs);

	send_log(NULL, LOG_NOTICE,"acme: %s: Successful update of the certificate.\n", ctx->store->path);


	line[0] = ist("acme newcert ");
	line[1] = ist(ctx->store->path);
	line[2] = ist("\n\0");

	dpapi = sink_find("dpapi");
	if (dpapi)
		sink_write(dpapi, LOG_HEADER_NONE, 0, line, 3);

	ctx->store = NULL;

	ret = 0;

error:
	HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock);
	return ret;

}

int acme_res_certificate(struct task *task, struct acme_ctx *ctx, char **errmsg)
{
	struct httpclient *hc;
	struct http_hdr *hdrs, *hdr;
	struct buffer *t1 = NULL, *t2 = NULL;
	int ret = 1;
	EVP_PKEY *key = NULL;

	hc = ctx->hc;
	if (!hc)
		goto error;

        if ((t1 = alloc_trash_chunk()) == NULL)
		goto error;
        if ((t2 = alloc_trash_chunk()) == NULL)
		goto error;

	hdrs = hc->res.hdrs;

	for (hdr = hdrs; isttest(hdr->v); hdr++) {
		if (isteqi(hdr->n, ist("Replay-Nonce"))) {
			istfree(&ctx->nonce);
			ctx->nonce = istdup(hdr->v);
		}
		/* get the next retry timing */
		if (isteqi(hdr->n, ist("Retry-After"))) {
			ctx->retryafter = __strl2uic(hdr->v.ptr, hdr->v.len);
		}
	}

	TRACE_DATA(__FUNCTION__, ACME_EV_RES, ctx, NULL, &hc->res.buf);

	if (hc->res.status < 200 || hc->res.status >= 300) {
		if ((ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.detail", t1->area, t1->size)) > -1)
			t1->data = ret;
		if ((ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.type", t2->area, t2->size)) > -1)
			t2->data = ret;

		if (t2->data && t1->data)
			memprintf(errmsg, "invalid HTTP status code %d when getting challenge URL: \"%.*s\" (%.*s)", hc->res.status, (int)t1->data, t1->area, (int)t2->data, t2->area);
		else
			memprintf(errmsg, "invalid HTTP status code %d when getting challengge URL", hc->res.status);
		goto error;
	}

	/* loading a PEM would remove the key, save it for later */
	key = ctx->store->data->key;
	ctx->store->data->key = NULL;

	/* XXX: might need a function dedicated to this, which does not read a private key */
	if (ssl_sock_load_pem_into_ckch(ctx->store->path, hc->res.buf.area, ctx->store->data , errmsg) != 0)
		goto error;

	/* restore the key */
	ctx->store->data->key = key;
	key = NULL;

	if (acme_update_certificate(task, ctx, errmsg) != 0)
		goto error;

out:
	ret = 0;

error:
	if (key)
		ctx->store->data->key = key;
	free_trash_chunk(t1);
	free_trash_chunk(t2);
	httpclient_destroy(hc);
	ctx->hc = NULL;

	return ret;
}

int acme_res_chkorder(struct task *task, struct acme_ctx *ctx, char **errmsg)
{
	struct httpclient *hc;
	struct http_hdr *hdrs, *hdr;
	struct buffer *t1 = NULL, *t2 = NULL;
	int ret = 1;

	hc = ctx->hc;
	if (!hc)
		goto error;

        if ((t1 = alloc_trash_chunk()) == NULL)
		goto error;
        if ((t2 = alloc_trash_chunk()) == NULL)
		goto error;

	hdrs = hc->res.hdrs;

	for (hdr = hdrs; hdrs && isttest(hdr->v); hdr++) {
		if (isteqi(hdr->n, ist("Replay-Nonce"))) {
			istfree(&ctx->nonce);
			ctx->nonce = istdup(hdr->v);
		}
		/* get the next retry timing */
		if (isteqi(hdr->n, ist("Retry-After"))) {
			ctx->retryafter = __strl2uic(hdr->v.ptr, hdr->v.len);
		}
	}

	TRACE_DATA(__FUNCTION__, ACME_EV_RES, ctx, NULL, &hc->res.buf);

	if (hc->res.status < 200 || hc->res.status >= 300) {
		if ((ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.detail", t1->area, t1->size)) > -1)
			t1->data = ret;
		if ((ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.type", t2->area, t2->size)) > -1)
			t2->data = ret;

		if (t2->data && t1->data)
			memprintf(errmsg, "invalid HTTP status code %d when getting Order URL: \"%.*s\" (%.*s)", hc->res.status, (int)t1->data, t1->area, (int)t2->data, t2->area);
		else
			memprintf(errmsg, "invalid HTTP status code %d when getting Order URL", hc->res.status);
		goto error;
	}
	ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.certificate", trash.area, trash.size);
	if (ret == -1) {
		memprintf(errmsg, "couldn't get a the certificate URL");
		goto error;
	}
	trash.data = ret;
	ctx->certificate = istdup(ist2(trash.area, trash.data));
	if (!isttest(ctx->certificate)) {
		memprintf(errmsg, "out of memory");
		goto error;
	}
	ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.status", trash.area, trash.size);
	if (ret == -1) {
		memprintf(errmsg, "couldn't get a the Order status");
		goto error;
	}
	trash.data = ret;
	if (strncasecmp("valid", trash.area, trash.data) != 0) {
		memprintf(errmsg, "order status: %.*s", (int)trash.data, trash.area);
		goto error;
	};

	ret = 0;

error:
	free_trash_chunk(t1);
	free_trash_chunk(t2);
	httpclient_destroy(hc);
	ctx->hc = NULL;

	return ret;
}

/* Send the CSR over the Finalize URL */
int acme_req_finalize(struct task *task, struct acme_ctx *ctx, char **errmsg)
{
	X509_REQ *req = ctx->req;
	struct buffer *csr = NULL;
	struct buffer *req_in = NULL;
	struct buffer *req_out = NULL;
	const struct http_hdr hdrs[] = {
		{ IST("Content-Type"), IST("application/jose+json") },
		{ IST_NULL, IST_NULL }
	};
	int ret = 1;
	size_t len = 0;
	unsigned char *data = NULL;

        if ((csr = alloc_trash_chunk()) == NULL)
		goto error;
        if ((req_in = alloc_trash_chunk()) == NULL)
		goto error;
        if ((req_out = alloc_trash_chunk()) == NULL)
		goto error;

	len = i2d_X509_REQ(req, &data);
	if (len <= 0)
		goto error;

	ret = a2base64url((char *)data, len, csr->area, csr->size);
	if (ret <= 0)
		goto error;
	csr->data = ret;

	chunk_printf(req_in, "{ \"csr\": \"%.*s\" }", (int)csr->data, csr->area);


	if (acme_jws_payload(req_in, ctx->nonce, ctx->finalize, ctx->cfg->account.pkey, ctx->kid, req_out, errmsg) != 0)
		goto error;

	if (acme_http_req(task, ctx, ctx->finalize, HTTP_METH_POST, hdrs, ist2(req_out->area, req_out->data)))
		goto error;

	ret = 0;
	goto out;
error:
	memprintf(errmsg, "couldn't request the finalize URL");
out:
	OPENSSL_free(data);
	free_trash_chunk(req_in);
	free_trash_chunk(req_out);
	free_trash_chunk(csr);

	return ret;

}

int acme_res_finalize(struct task *task, struct acme_ctx *ctx, char **errmsg)
{
	struct httpclient *hc;
	struct http_hdr *hdrs, *hdr;
	struct buffer *t1 = NULL, *t2 = NULL;
	int ret = 1;

	hc = ctx->hc;
	if (!hc)
		goto error;

        if ((t1 = alloc_trash_chunk()) == NULL)
		goto error;
        if ((t2 = alloc_trash_chunk()) == NULL)
		goto error;

	hdrs = hc->res.hdrs;

	for (hdr = hdrs; isttest(hdr->v); hdr++) {
		if (isteqi(hdr->n, ist("Replay-Nonce"))) {
			istfree(&ctx->nonce);
			ctx->nonce = istdup(hdr->v);
		}
		/* get the next retry timing */
		if (isteqi(hdr->n, ist("Retry-After"))) {
			ctx->retryafter = __strl2uic(hdr->v.ptr, hdr->v.len);
		}
	}

	TRACE_DATA(__FUNCTION__, ACME_EV_RES, ctx, NULL, &hc->res.buf);

	if (hc->res.status < 200 || hc->res.status >= 300) {
		if ((ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.detail", t1->area, t1->size)) > -1)
			t1->data = ret;
		if ((ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.type", t2->area, t2->size)) > -1)
			t2->data = ret;

		if (t2->data && t1->data)
			memprintf(errmsg, "invalid HTTP status code %d when getting Finalize URL: \"%.*s\" (%.*s)", hc->res.status, (int)t1->data, t1->area, (int)t2->data, t2->area);
		else
			memprintf(errmsg, "invalid HTTP status code %d when getting Finalize URL", hc->res.status);
		goto error;
	}

	ret = 0;

error:
	free_trash_chunk(t1);
	free_trash_chunk(t2);
	httpclient_destroy(hc);
	ctx->hc = NULL;

	return ret;
}

/*
 * Send the READY request for the challenge
 */
int acme_req_challenge(struct task *task, struct acme_ctx *ctx, struct acme_auth *auth, char **errmsg)
{
	struct buffer *req_in = NULL;
	struct buffer *req_out = NULL;
	const struct http_hdr hdrs[] = {
		{ IST("Content-Type"), IST("application/jose+json") },
		{ IST_NULL, IST_NULL }
	};
	int ret = 1;

        if ((req_in = alloc_trash_chunk()) == NULL)
		goto error;
        if ((req_out = alloc_trash_chunk()) == NULL)
		goto error;

	chunk_printf(req_in, "{}");

	TRACE_DATA("REQ challenge dec", ACME_EV_REQ, ctx, &auth->chall, req_in);

	if (acme_jws_payload(req_in, ctx->nonce, auth->chall, ctx->cfg->account.pkey, ctx->kid, req_out, errmsg) != 0)
		goto error;

	TRACE_DATA("REQ challenge enc", ACME_EV_REQ, ctx, &auth->chall, req_out);

	if (acme_http_req(task, ctx, auth->chall, HTTP_METH_POST, hdrs, ist2(req_out->area, req_out->data)))
		goto error;

	ret = 0;
	goto out;
error:
	memprintf(errmsg, "couldn't generate the Challenge request");
out:
	free_trash_chunk(req_in);
	free_trash_chunk(req_out);

	return ret;

}

/* parse the challenge URL response */
enum acme_ret acme_res_challenge(struct task *task, struct acme_ctx *ctx, struct acme_auth *auth, int chk, char **errmsg)
{
	struct httpclient *hc;
	struct http_hdr *hdrs, *hdr;
	struct buffer *t1 = NULL, *t2 = NULL;
	enum acme_ret ret = ACME_RET_FAIL;
	int res = 0;

	hc = ctx->hc;
	if (!hc)
		goto out;

        if ((t1 = alloc_trash_chunk()) == NULL)
		goto out;
        if ((t2 = alloc_trash_chunk()) == NULL)
		goto out;

	hdrs = hc->res.hdrs;

	TRACE_DATA(__FUNCTION__, ACME_EV_RES, ctx, NULL, &hc->res.buf);

	for (hdr = hdrs; isttest(hdr->v); hdr++) {
		if (isteqi(hdr->n, ist("Replay-Nonce"))) {
			istfree(&ctx->nonce);
			ctx->nonce = istdup(hdr->v);
		}
		/* get the next retry timing */
		if (isteqi(hdr->n, ist("Retry-After"))) {
			ctx->retryafter = __strl2uic(hdr->v.ptr, hdr->v.len);
		}
	}

	res = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.status", trash.area, trash.size);
	if (res == -1) {
		memprintf(errmsg, "waiting for the status");
		ret = ACME_RET_RETRY;
		goto out;
	}
	trash.data = res;

	if (strncasecmp("pending", trash.area, trash.data) == 0 || strncasecmp("processing", trash.area, trash.data) == 0) {
		if (chk) { /* during challenge chk */
			memprintf(errmsg, "challenge status: %.*s", (int)trash.data, trash.area);
			ret = ACME_RET_RETRY;
			goto out;
		} else {  /* during object creation */
			ret = ACME_RET_OK;
			goto out;
		}
	}

	if (strncasecmp("valid", trash.area, trash.data) == 0) {
		ret = ACME_RET_OK;
		goto out;
	}

	if (hc->res.status < 200 || hc->res.status >= 300 || mjson_find(hc->res.buf.area, hc->res.buf.data, "$.error", NULL, NULL) == MJSON_TOK_OBJECT) {
		/* XXX: need a generic URN error parser */
		if ((res = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.error.detail", t1->area, t1->size)) > -1)
			t1->data = res;
		if ((res = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.error.type", t2->area, t2->size)) > -1)
			t2->data = res;
		if (t2->data && t1->data)
			memprintf(errmsg, "challenge error: \"%.*s\" (%.*s) (HTTP status code %d)", (int)t1->data, t1->area, (int)t2->data, t2->area, hc->res.status);
		else
			memprintf(errmsg, "challenge error: unknown (HTTP status code %d)", hc->res.status);
		goto out;
	}

out:
	free_trash_chunk(t1);
	free_trash_chunk(t2);
	httpclient_destroy(hc);
	ctx->hc = NULL;

	return ret;
}

/* generate a POST-as-GET request */
int acme_post_as_get(struct task *task, struct acme_ctx *ctx, struct ist url, char **errmsg)
{
	struct buffer *req_in = NULL;
	struct buffer *req_out = NULL;
	const struct http_hdr hdrs[] = {
		{ IST("Content-Type"), IST("application/jose+json") },
		{ IST_NULL, IST_NULL }
	};
	int ret = 1;

        if ((req_in = alloc_trash_chunk()) == NULL)
		goto error_alloc;
        if ((req_out = alloc_trash_chunk()) == NULL)
		goto error_alloc;

	TRACE_USER("POST-as-GET    ", ACME_EV_REQ, ctx, &url);

	/* empty payload */
	if (acme_jws_payload(req_in, ctx->nonce, url, ctx->cfg->account.pkey, ctx->kid, req_out, errmsg) != 0)
		goto error_jws;

	TRACE_DATA("POST-as-GET enc", ACME_EV_REQ, ctx, &url, req_out);

	if (acme_http_req(task, ctx, url, HTTP_METH_POST, hdrs, ist2(req_out->area, req_out->data)))
		goto error_http;

	ret = 0;

	goto end;

error_jws:
	memprintf(errmsg, "couldn't generate the JWS token: %s", errmsg ? *errmsg : "");
	goto end;

error_http:
	memprintf(errmsg, "couldn't generate the http request");
	goto end;

error_alloc:
	memprintf(errmsg, "couldn't allocate memory");
	goto end;

end:
	free_trash_chunk(req_in);
	free_trash_chunk(req_out);

	return ret;
}

int acme_res_auth(struct task *task, struct acme_ctx *ctx, struct acme_auth *auth, char **errmsg)
{
	struct httpclient *hc;
	struct http_hdr *hdrs, *hdr;
	struct buffer *t1 = NULL, *t2 = NULL;
	int ret = 1;
	int i;

	hc = ctx->hc;
	if (!hc)
		goto error;

        if ((t1 = alloc_trash_chunk()) == NULL)
		goto error;
        if ((t2 = alloc_trash_chunk()) == NULL)
		goto error;

	hdrs = hc->res.hdrs;

	for (hdr = hdrs; isttest(hdr->v); hdr++) {
		if (isteqi(hdr->n, ist("Replay-Nonce"))) {
			istfree(&ctx->nonce);
			ctx->nonce = istdup(hdr->v);
		}
		/* get the next retry timing */
		if (isteqi(hdr->n, ist("Retry-After"))) {
			ctx->retryafter = __strl2uic(hdr->v.ptr, hdr->v.len);
		}

	}
	TRACE_DATA(__FUNCTION__, ACME_EV_RES, ctx, NULL, &hc->res.buf);

	if (hc->res.status < 200 || hc->res.status >= 300) {
		/* XXX: need a generic URN error parser */
		if ((ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.detail", t1->area, t1->size)) > -1)
			t1->data = ret;
		if ((ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.type", t2->area, t2->size)) > -1)
			t2->data = ret;
		if (t2->data && t1->data)
			memprintf(errmsg, "invalid HTTP status code %d when getting Authorization URL: \"%.*s\" (%.*s)", hc->res.status, (int)t1->data, t1->area, (int)t2->data, t2->area);
		else
			memprintf(errmsg, "invalid HTTP status code %d when getting Authorization URL", hc->res.status);
		goto error;
	}

	/* check and save the DNS entry */
	ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.identifier.type", t1->area, t1->size);
	if (ret == -1) {
		memprintf(errmsg, "couldn't get a type \"dns\" from Authorization URL \"%s\"", auth->auth.ptr);
		goto error;
	}
	t1->data = ret;

	ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.identifier.value", t2->area, t2->size);
	if (ret == -1) {
		memprintf(errmsg, "couldn't get a type \"dns\" from Authorization URL \"%s\"", auth->auth.ptr);
		goto error;
	}
	t2->data = ret;

	auth->dns = istdup(ist2(t2->area, t2->data));

	ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.status", trash.area, trash.size);
	if (ret == -1) {
		memprintf(errmsg, "couldn't get a \"status\" from Authorization URL \"%s\"", auth->auth.ptr);
		goto error;
	}
	trash.data = ret;

	/* if auth is already valid we need to skip solving challenges */
	if (strncasecmp("valid", trash.area, trash.data) == 0) {
		auth->validated = 1;
		goto out;
	}

	/* get the multiple challenges and select the one from the configuration */
	for (i = 0; ; i++) {
		int ret;
		char chall[] = "$.challenges[XXX]";
		const char *tokptr;
		int toklen;

		if (snprintf(chall, sizeof(chall), "$.challenges[%d]", i) >= sizeof(chall))
			goto error;

		/* break the loop at the end of the challenges objects list */
		if (mjson_find(hc->res.buf.area, hc->res.buf.data, chall, &tokptr, &toklen) == MJSON_TOK_INVALID)
			break;

		ret = mjson_get_string(tokptr, toklen, "$.type", trash.area, trash.size);
		if (ret == -1) {
			memprintf(errmsg, "couldn't get a challenge type in challenges[%d] from Authorization URL \"%s\"", i, auth->auth.ptr);
			goto error;
		}
		trash.data = ret;

		/* skip until this is the challenge we need */
		if (strncasecmp(ctx->cfg->challenge, trash.area, trash.data) != 0)
			continue;

		ret = mjson_get_string(tokptr, toklen, "$.url", trash.area, trash.size);
		if (ret == -1) {
			memprintf(errmsg, "couldn't get a challenge URL in challenges[%d] from Authorization URL \"%s\"", i, auth->auth.ptr);
			goto error;
		}
		trash.data = ret;
		auth->chall = istdup(ist2(trash.area, trash.data));
		if (!isttest(auth->chall)) {
			memprintf(errmsg, "out of memory");
			goto error;
		}

		ret = mjson_get_string(tokptr, toklen, "$.token", trash.area, trash.size);
		if (ret == -1) {
			memprintf(errmsg, "couldn't get a token in challenges[%d] from Authorization URL \"%s\"", i, auth->auth.ptr);
			goto error;
		}
		trash.data = ret;
		auth->token = istdup(ist2(trash.area, trash.data));
		if (!isttest(auth->token)) {
			memprintf(errmsg, "out of memory");
			goto error;
		}

		/* compute a response for the TXT entry */
		if (strcasecmp(ctx->cfg->challenge, "dns-01") == 0) {
			struct sink *dpapi;
			struct ist line[16];
			int nmsg = 0;
			struct buffer *dns_record = NULL;

			dns_record = get_trash_chunk();

			if (acme_txt_record(ist(ctx->cfg->account.thumbprint), auth->token, dns_record) == 0) {
				memprintf(errmsg, "couldn't compute the dns-01 challenge");
				goto error;
			}

			/* replace the token by the TXT entry */
			istfree(&auth->token);
			auth->token = istdup(ist2(dns_record->area, dns_record->data));

			if (ctx->cfg->cond_ready & ACME_RDY_CLI)
				send_log(NULL, LOG_NOTICE,"acme: %s: dns-01 requires to set the \"_acme-challenge.%.*s\" TXT record to \"%.*s\" and use the \"acme challenge_ready %s domain %.*s\" command over the CLI\n",
			                                             ctx->store->path, (int)auth->dns.len, auth->dns.ptr, (int)auth->token.len, auth->token.ptr, ctx->store->path, (int)auth->dns.len, auth->dns.ptr);

			/* dump to the "dpapi" sink */
			line[nmsg++] = ist("acme deploy ");
			line[nmsg++] = ist(ctx->store->path);
			line[nmsg++] = ist(" thumbprint ");
			line[nmsg++] = ist(ctx->cfg->account.thumbprint);
			line[nmsg++] = ist("\n");

			if (ctx->cfg->provider) {
				line[nmsg++] = ist("provider-name \"");
				line[nmsg++] = ist(ctx->cfg->provider);
				line[nmsg++] = ist("\"\n");
			}
			if (ctx->cfg->vars) {
				line[nmsg++] = ist("acme-vars \"");
				line[nmsg++] = ist(ctx->cfg->vars);
				line[nmsg++] = ist("\"\n");
			}
			if (auth->dns.ptr) {
				line[nmsg++] = ist("dns-01-record \"");
				line[nmsg++] = ist2(dns_record->area, dns_record->data);
				line[nmsg++] = ist("\"\n");
			}

			line[nmsg++] = ist2( hc->res.buf.area, hc->res.buf.data); /* dump the HTTP response */
			line[nmsg++] = ist("\n\0");

			dpapi = sink_find("dpapi");
			if (dpapi)
				sink_write(dpapi, LOG_HEADER_NONE, 0, line, nmsg);
		} else {
			/* only useful for http-01 */
			if (acme_add_challenge_map(ctx->cfg->map, auth->token.ptr, ctx->cfg->account.thumbprint, errmsg) != 0) {
				memprintf(errmsg, "couldn't add the token to the '%s' map: %s", ctx->cfg->map, *errmsg);
				goto error;
			}
		}

		/* we only need one challenge, and iteration is only used to found the right one */
		break;
	}

out:
	ret = 0;

error:
	free_trash_chunk(t1);
	free_trash_chunk(t2);
	httpclient_destroy(hc);
	ctx->hc = NULL;

	return ret;
}


int acme_req_neworder(struct task *task, struct acme_ctx *ctx, char **errmsg)
{
	struct buffer *req_in = NULL;
	struct buffer *req_out = NULL;
	const struct http_hdr hdrs[] = {
		{ IST("Content-Type"), IST("application/jose+json") },
		{ IST_NULL, IST_NULL }
	};
	int ret = 1;
	char **san = ctx->store->conf.acme.domains;

        if ((req_in = alloc_trash_chunk()) == NULL)
		goto error;
        if ((req_out = alloc_trash_chunk()) == NULL)
		goto error;

	chunk_printf(req_in, "{ \"identifiers\": [ ");

	if (!san)
		goto error;

	for (; san && *san; san++) {
//		fprintf(stderr, "%s:%d %s\n", __FUNCTION__, __LINE__, *san);
		chunk_appendf(req_in, "%s{ \"type\": \"dns\",  \"value\": \"%s\" }", (*san == *ctx->store->conf.acme.domains) ?  "" : ",", *san);
	}

	chunk_appendf(req_in, " ] }");

	TRACE_DATA("NewOrder Decode", ACME_EV_REQ, ctx, &ctx->resources.newOrder, req_in);


	if (acme_jws_payload(req_in, ctx->nonce, ctx->resources.newOrder, ctx->cfg->account.pkey, ctx->kid, req_out, errmsg) != 0)
		goto error;

	TRACE_DATA("NewOrder JWS    ", ACME_EV_REQ, ctx, &ctx->resources.newOrder, req_out);
	if (acme_http_req(task, ctx, ctx->resources.newOrder, HTTP_METH_POST, hdrs, ist2(req_out->area, req_out->data)))
		goto error;

	ret = 0;
	goto out;
error:
	memprintf(errmsg, "couldn't generate the newOrder request");
out:
	free_trash_chunk(req_in);
	free_trash_chunk(req_out);

	return ret;

}

int acme_res_neworder(struct task *task, struct acme_ctx *ctx, char **errmsg)
{
	struct httpclient *hc;
	struct http_hdr *hdrs, *hdr;
	struct buffer *t1 = NULL, *t2 = NULL;
	int ret = 1;
	int i;

	hc = ctx->hc;
	if (!hc)
		goto error;

        if ((t1 = alloc_trash_chunk()) == NULL)
		goto error;
        if ((t2 = alloc_trash_chunk()) == NULL)
		goto error;

	hdrs = hc->res.hdrs;

	for (hdr = hdrs; isttest(hdr->v); hdr++) {
		if (isteqi(hdr->n, ist("Replay-Nonce"))) {
			istfree(&ctx->nonce);
			ctx->nonce = istdup(hdr->v);
		}
		/* get the next retry timing */
		if (isteqi(hdr->n, ist("Retry-After"))) {
			ctx->retryafter = __strl2uic(hdr->v.ptr, hdr->v.len);
		}
		/* get the order URL */
		if (isteqi(hdr->n, ist("Location"))) {
			istfree(&ctx->order);
			ctx->order = istdup(hdr->v);
		}
	}
	TRACE_DATA(__FUNCTION__, ACME_EV_RES, ctx, NULL, &hc->res.buf);

	if (hc->res.status < 200 || hc->res.status >= 300) {
		if ((ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.detail", t1->area, t1->size)) > -1)
			t1->data = ret;
		if ((ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.type", t2->area, t2->size)) > -1)
			t2->data = ret;
		if (t2->data && t1->data)
			memprintf(errmsg, "invalid HTTP status code %d when getting newOrder URL: \"%.*s\" (%.*s)", hc->res.status, (int)t1->data, t1->area, (int)t2->data, t2->area);
		else
			memprintf(errmsg, "invalid HTTP status code %d when getting newOrder URL", hc->res.status);
		goto error;
	}

	if (!isttest(ctx->order)) {
		memprintf(errmsg, "couldn't get an order Location during newOrder");
		goto error;
	}
	/* get the multiple authorizations URL and tokens */
	for (i = 0; ; i++) {
		struct acme_auth *auth;
		char url[] = "$.authorizations[XXX]";

		if (snprintf(url, sizeof(url), "$.authorizations[%d]", i) >= sizeof(url)) {
			memprintf(errmsg, "couldn't loop on authorizations during newOrder");
			goto error;
		}

		ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, url, trash.area, trash.size);
		if (ret == -1) /* end of the authorizations array */
			break;
		trash.data = ret;

		if ((auth = calloc(1, sizeof(*auth))) == NULL) {
			memprintf(errmsg, "out of memory");
			goto error;
		}

		auth->auth = istdup(ist2(trash.area, trash.data));
		if (!isttest(auth->auth)) {
			memprintf(errmsg, "out of memory");
			goto error;
		}

		auth->next = ctx->auths;
		ctx->auths = auth;
		ctx->next_auth = auth;
	}

	if ((ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.finalize", trash.area, trash.size)) <= 0) {
		memprintf(errmsg, "couldn't find the finalize URL");
		goto error;
	}
	trash.data = ret;
	istfree(&ctx->finalize);
	ctx->finalize = istdup(ist2(trash.area, trash.data));
	if (!isttest(ctx->finalize)) {
		memprintf(errmsg, "out of memory");
		goto error;
	}

	ret = 0;

error:
	free_trash_chunk(t1);
	free_trash_chunk(t2);
	httpclient_destroy(hc);
	ctx->hc = NULL;

	return ret;
}


int acme_req_account(struct task *task, struct acme_ctx *ctx, int newaccount, char **errmsg)
{
	struct buffer *req_in = NULL;
	struct buffer *req_out = NULL;
	const struct http_hdr hdrs[] = {
		{ IST("Content-Type"), IST("application/jose+json") },
		{ IST_NULL, IST_NULL }
	};
	char *accountreq = "{\n"
		"    \"termsOfServiceAgreed\": true,\n"
		"    \"onlyReturnExisting\":   true\n"
		"}\n";
	char *newaccountreq = "{\n"
		"    \"termsOfServiceAgreed\": true,\n"
		"    \"contact\": [\n"
		"        \"mailto:%s\"\n"
		"    ]\n"
		"}\n";
	int ret = 1;

        if ((req_in = alloc_trash_chunk()) == NULL)
		goto error;
        if ((req_out = alloc_trash_chunk()) == NULL)
		goto error;

	if (newaccount)
		chunk_printf(req_in, newaccountreq, ctx->cfg->account.contact);
	else
		chunk_printf(req_in, "%s", accountreq);

	TRACE_DATA("newAccount Decoded", ACME_EV_REQ, ctx, &ctx->resources.newAccount, req_in);

	if (acme_jws_payload(req_in, ctx->nonce, ctx->resources.newAccount, ctx->cfg->account.pkey, ctx->kid, req_out, errmsg) != 0)
		goto error;

	if (acme_http_req(task, ctx, ctx->resources.newAccount, HTTP_METH_POST, hdrs, ist2(req_out->area, req_out->data)))
		goto error;

	ret = 0;
	goto out;
error:
	memprintf(errmsg, "couldn't generate the newAccount request");
out:
	free_trash_chunk(req_in);
	free_trash_chunk(req_out);

	return ret;
}

int acme_res_account(struct task *task, struct acme_ctx *ctx, int newaccount, char **errmsg)
{
	struct httpclient *hc;
	struct http_hdr *hdrs, *hdr;
	struct buffer *t1 = NULL, *t2 = NULL;
	int ret = 1;

	hc = ctx->hc;
	if (!hc)
		goto error;

        if ((t1 = alloc_trash_chunk()) == NULL)
		goto error;
        if ((t2 = alloc_trash_chunk()) == NULL)
		goto error;

	hdrs = hc->res.hdrs;

	for (hdr = hdrs; isttest(hdr->v); hdr++) {
		if (isteqi(hdr->n, ist("Location"))) {
			istfree(&ctx->kid);
			ctx->kid = istdup(hdr->v);
		}
		/* get the next retry timing */
		if (isteqi(hdr->n, ist("Retry-After"))) {
			ctx->retryafter = __strl2uic(hdr->v.ptr, hdr->v.len);
		}
		if (isteqi(hdr->n, ist("Replay-Nonce"))) {
			istfree(&ctx->nonce);
			ctx->nonce = istdup(hdr->v);
		}
	}

	TRACE_DATA(__FUNCTION__, ACME_EV_RES, ctx, NULL, &hc->res.buf);

	if (hc->res.status < 200 || hc->res.status >= 300) {
		if ((ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.detail", t1->area, t1->size)) > -1)
			t1->data = ret;
		if ((ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.type", t2->area, t2->size)) > -1)
			t2->data = ret;

		if (!newaccount) {
			/* not an error, we only need to create a new account */
			if (strcmp("urn:ietf:params:acme:error:accountDoesNotExist", t2->area) == 0)
				goto out;
		}

		if (t2->data && t1->data)
			memprintf(errmsg, "invalid HTTP status code %d when getting Account URL: \"%.*s\" (%.*s)", hc->res.status, (int)t1->data, t1->area, (int)t2->data, t2->area);
		else
			memprintf(errmsg, "invalid HTTP status code %d when getting Account URL", hc->res.status);
		goto error;
	}
out:
	ret = 0;

error:
	free_trash_chunk(t1);
	free_trash_chunk(t2);
	httpclient_destroy(hc);
	ctx->hc = NULL;

	return ret;
}



int acme_nonce(struct task *task, struct acme_ctx *ctx, char **errmsg)
{
	struct httpclient *hc;
	struct http_hdr *hdrs, *hdr;

	hc = ctx->hc;
	if (!hc)
		goto error;

	if (hc->res.status < 200 || hc->res.status >= 300) {
		memprintf(errmsg, "invalid HTTP status code %d when getting Nonce URL", hc->res.status);
		goto error;
	}

	TRACE_DATA(__FUNCTION__, ACME_EV_RES, ctx, NULL, &hc->res.buf);

	hdrs = hc->res.hdrs;

	for (hdr = hdrs; isttest(hdr->v); hdr++) {
		if (isteqi(hdr->n, ist("Replay-Nonce"))) {
			istfree(&ctx->nonce);
			ctx->nonce = istdup(hdr->v);
//			fprintf(stderr, "Replay-Nonce: %.*s\n", (int)hdr->v.len, hdr->v.ptr);

		}
	}

	httpclient_destroy(hc);
	ctx->hc = NULL;

	return 0;

error:
	httpclient_destroy(hc);
	ctx->hc = NULL;

	return 1;
}

int acme_directory(struct task *task, struct acme_ctx *ctx, char **errmsg)
{
	struct httpclient *hc;
	int ret = 0;

	hc = ctx->hc;

	if (!hc)
		goto error;

	if (hc->res.status != 200) {
		memprintf(errmsg, "invalid HTTP status code %d when getting directory URL", hc->res.status);
		goto error;
	}

	TRACE_DATA(__FUNCTION__, ACME_EV_RES, ctx, NULL, &hc->res.buf);

	if ((ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.newNonce", trash.area, trash.size)) <= 0) {
		memprintf(errmsg, "couldn't get newNonce URL from the directory URL");
		goto error;
	}
	ctx->resources.newNonce = istdup(ist2(trash.area, ret));
	if (!isttest(ctx->resources.newNonce)) {
		memprintf(errmsg, "couldn't get newNonce URL from the directory URL");
		goto error;
	}

	if ((ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.newAccount", trash.area, trash.size)) <= 0) {
		memprintf(errmsg, "couldn't get newAccount URL from the directory URL");
		goto error;
	}
	ctx->resources.newAccount = istdup(ist2(trash.area, ret));
	if (!isttest(ctx->resources.newAccount)) {
		memprintf(errmsg, "couldn't get newAccount URL from the directory URL");
		goto error;
	}
	if ((ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.newOrder", trash.area, trash.size)) <= 0) {
		memprintf(errmsg, "couldn't get newOrder URL from the directory URL");
		goto error;
	}
	ctx->resources.newOrder = istdup(ist2(trash.area, ret));
	if (!isttest(ctx->resources.newOrder)) {
		memprintf(errmsg, "couldn't get newOrder URL from the directory URL");
		goto error;
	}

	httpclient_destroy(hc);
	ctx->hc = NULL;

//	fprintf(stderr, "newNonce: %s\nnewAccount: %s\nnewOrder: %s\n",
//	        ctx->resources.newNonce.ptr, ctx->resources.newAccount.ptr, ctx->resources.newOrder.ptr);

	return 0;

error:
	httpclient_destroy(hc);
	ctx->hc = NULL;

	istfree(&ctx->resources.newNonce);
	istfree(&ctx->resources.newAccount);
	istfree(&ctx->resources.newOrder);

	return 1;
}

/*
 * Task for ACME processing:
 *  - when retrying after a failure, the task must be waked up
 *  - when calling a get function, the httpclient is waking up the task again
 * once the data are ready or upon failure
 */
struct task *acme_process(struct task *task, void *context, unsigned int state)
{
	struct acme_ctx *ctx = task->context;
	enum acme_st st = ctx->state;
	enum http_st http_st = ctx->http_state;
	char *errmsg = NULL;

re:
	TRACE_USER("ACME Task Handle", ACME_EV_TASK, ctx, &st);

	switch (st) {
		case ACME_RESOURCES:
			if (http_st == ACME_HTTP_REQ) {
				if (acme_http_req(task, ctx, ist(ctx->cfg->directory), HTTP_METH_GET, NULL, IST_NULL) != 0)
					goto retry;
			}

			if (http_st == ACME_HTTP_RES) {
				if (acme_directory(task, ctx, &errmsg) != 0) {
					goto retry;
				}
				st = ACME_NEWNONCE;
				goto nextreq;
			}
		break;
		case ACME_NEWNONCE:
			if (http_st == ACME_HTTP_REQ) {
				if (acme_http_req(task, ctx, ctx->resources.newNonce, HTTP_METH_HEAD, NULL, IST_NULL) != 0)
					goto retry;
			}
			if (http_st == ACME_HTTP_RES) {
				if (acme_nonce(task, ctx, &errmsg) != 0) {
					goto retry;
				}
				st = ACME_CHKACCOUNT;
				goto nextreq;
			}

		break;
		case ACME_CHKACCOUNT:
			if (http_st == ACME_HTTP_REQ) {
				if (acme_req_account(task, ctx, 0, &errmsg) != 0)
					goto retry;
			}
			if (http_st == ACME_HTTP_RES) {
				if (acme_res_account(task, ctx, 0, &errmsg) != 0) {
					goto retry;
				}
				if (!isttest(ctx->kid))
					st = ACME_NEWACCOUNT;
				else
					st = ACME_NEWORDER;
				goto nextreq;
			}
		break;
		case ACME_NEWACCOUNT:
			if (http_st == ACME_HTTP_REQ) {
				if (acme_req_account(task, ctx, 1, &errmsg) != 0)
					goto retry;
			}
			if (http_st == ACME_HTTP_RES) {
				if (acme_res_account(task, ctx, 1, &errmsg) != 0) {
					goto retry;
				}
				st = ACME_NEWORDER;
				goto nextreq;
			}


		break;
		case ACME_NEWORDER:
			if (http_st == ACME_HTTP_REQ) {
				if (acme_req_neworder(task, ctx, &errmsg) != 0)
					goto retry;
			}
			if (http_st == ACME_HTTP_RES) {
				if (acme_res_neworder(task, ctx, &errmsg) != 0) {
					goto retry;
				}
				st = ACME_AUTH;
				goto nextreq;
			}
		break;
		case ACME_AUTH:
			if (http_st == ACME_HTTP_REQ) {
				if (acme_post_as_get(task, ctx, ctx->next_auth->auth, &errmsg) != 0)
					goto retry;
			}
			if (http_st == ACME_HTTP_RES) {
				if (acme_res_auth(task, ctx, ctx->next_auth, &errmsg) != 0) {
					goto retry;
				}
				if ((ctx->next_auth = ctx->next_auth->next) == NULL) {
					if (strcasecmp(ctx->cfg->challenge, "dns-01") == 0 && ctx->cfg->cond_ready)
						st = ACME_CLI_WAIT;
					else
						st = ACME_CHALLENGE;
					ctx->next_auth = ctx->auths;
				}
				/* call with next auth or do the challenge step */
				goto nextreq;
			}
		break;
		case ACME_CLI_WAIT: {
			struct acme_auth *auth;
			int all_cond_ready = ctx->cfg->cond_ready;

			for (auth = ctx->auths; auth != NULL; auth = auth->next) {
				all_cond_ready &= auth->ready;
			}

			/* if everything is ready, let's do the challenge request */
			if ((all_cond_ready & ctx->cfg->cond_ready) == ctx->cfg->cond_ready) {
				st = ACME_CHALLENGE;
				ctx->http_state = ACME_HTTP_REQ;
				ctx->state = st;
				goto nextreq;
			}

			/* if we need to wait for the CLI, let's wait */
			if ((ctx->cfg->cond_ready & ACME_RDY_CLI) && !(all_cond_ready & ACME_RDY_CLI))
				goto wait;

			/* next step */
			st = ACME_INITIAL_DELAY;
			goto nextreq;
		}
		break;
		case ACME_INITIAL_DELAY: {
			struct acme_auth *auth;
			int all_cond_ready = ctx->cfg->cond_ready;

			for (auth = ctx->auths; auth != NULL; auth = auth->next) {
				all_cond_ready &= auth->ready;
			}

			/* if everything is ready, let's do the challenge request */
			if ((all_cond_ready & ctx->cfg->cond_ready) == ctx->cfg->cond_ready) {
				st = ACME_CHALLENGE;
				goto nextreq;
			}

			/* if we don't have an initial delay, let's trigger */
			if (!(ctx->cfg->cond_ready & ACME_RDY_DELAY)) {
				st = ACME_RSLV_TRIGGER;
				goto nextreq;
			}

			for (auth = ctx->auths; auth != NULL; auth = auth->next) {
				auth->ready |= ACME_RDY_DELAY;
			}

			/* either trigger the resolution of the challenge */
			if (ctx->cfg->cond_ready & ACME_RDY_DNS)
				st = ACME_RSLV_TRIGGER;
			else
				st = ACME_CHALLENGE;
			ctx->http_state = ACME_HTTP_REQ;
			ctx->state = st;
			send_log(NULL, LOG_NOTICE, "acme: %s: dns-01: waiting %ds\n",
			                            ctx->store->path, ctx->cfg->dns_delay);

			task->expire = tick_add(now_ms, ctx->cfg->dns_delay * 1000);
			return task;
		}
		break;
		case ACME_RSLV_RETRY_DELAY: {
			struct acme_auth *auth;
			int all_cond_ready = ctx->cfg->cond_ready;

			for (auth = ctx->auths; auth != NULL; auth = auth->next) {
				all_cond_ready &= auth->ready;
			}

			/* if everything is ready, let's do the challenge request */
			if ((all_cond_ready & ctx->cfg->cond_ready) == ctx->cfg->cond_ready) {
				st = ACME_CHALLENGE;
				goto nextreq;
			}

			/* Check if the next resolution would be triggered too
			 * late according to the dns_timeout and abort is
			 * necessary. */
			if (ctx->dnsstarttime && ns_to_sec(now_ns) + ctx->cfg->dns_delay > ctx->dnsstarttime + ctx->cfg->dns_timeout) {
				memprintf(&errmsg, "dns-01: Couldn't resolve the TXT records in %ds.",  ctx->cfg->dns_timeout);
				goto abort;
			}

			/* we don't need to wait, we can trigger the resolution
			 * after the delay */
			st = ACME_RSLV_TRIGGER;
			ctx->http_state = ACME_HTTP_REQ;
			ctx->state = st;
			send_log(NULL, LOG_NOTICE, "acme: %s: dns-01: retrying the resolution in %ds\n",
			                            ctx->store->path, ctx->cfg->dns_delay);

			task->expire = tick_add(now_ms, ctx->cfg->dns_delay * 1000);
			return task;
		}
		break;
		case ACME_RSLV_TRIGGER: {
			struct acme_auth *auth;

			/* set the start time of the DNS checks so we can apply
			 * the timeout */
			if (ctx->dnsstarttime == 0)
				 ctx->dnsstarttime = ns_to_sec(now_ns);
			/* on timer expiry, re-trigger resolution for non-ready auths */
			for (auth = ctx->auths; auth != NULL; auth = auth->next) {
				if (auth->ready == ctx->cfg->cond_ready)
					continue;

				HA_ATOMIC_INC(&ctx->dnstasks);

				auth->rslv = acme_rslv_start(auth, &ctx->dnstasks, &errmsg);
				if (!auth->rslv)
					goto abort;
				auth->rslv->acme_task = task;
			}
			st = ACME_RSLV_READY;
			goto wait;
		}
		break;
		case ACME_RSLV_READY: {
			struct acme_auth *auth;
			int all_ready = 1;

			/* if triggered by the CLI, wait for the DNS tasks to
			 * finish
			 */
                        if (HA_ATOMIC_LOAD(&ctx->dnstasks) != 0)
				goto wait;

			/* triggered by the latest DNS task */
			for (auth = ctx->auths; auth != NULL; auth = auth->next) {
				if (auth->ready == ctx->cfg->cond_ready)
					continue;
				if (auth->rslv->result != RSLV_STATUS_VALID) {
					send_log(NULL, LOG_NOTICE, "acme: %s: dns-01: Couldn't get the TXT record for \"_acme-challenge.%.*s\", expected \"%.*s\" (status=%d)\n",
					         ctx->store->path, (int)auth->dns.len, auth->dns.ptr,
					         (int)auth->token.len, auth->token.ptr,
					         auth->rslv->result);
					all_ready = 0;
				} else {
					if (isteq(auth->rslv->txt, auth->token)) {
						auth->ready |= ACME_RDY_DNS;
					} else {
						send_log(NULL, LOG_NOTICE, "acme: %s: dns-01: TXT record mismatch for \"_acme-challenge.%.*s\": expected \"%.*s\", got \"%.*s\"\n",
						         ctx->store->path, (int)auth->dns.len, auth->dns.ptr,
						         (int)auth->token.len, auth->token.ptr,
						         (int)auth->rslv->txt.len, auth->rslv->txt.ptr);
						all_ready = 0;
					}
				}
				acme_rslv_free(auth->rslv);
				auth->rslv = NULL;
			}
			if (all_ready) {
				st = ACME_CHALLENGE;
				ctx->next_auth = ctx->auths;
				goto nextreq;
			}

			/* not all ready yet, retry after dns-delay */
			st = ACME_RSLV_RETRY_DELAY;
			ctx->http_state = ACME_HTTP_REQ;
			ctx->state = st;
			goto nextreq;
		}
		break;
		case ACME_CHALLENGE:
			if (http_st == ACME_HTTP_REQ) {
				/* if challenge is already validated we skip this stage */
				if (ctx->next_auth->validated) {
					if ((ctx->next_auth = ctx->next_auth->next) == NULL) {
						st = ACME_CHKCHALLENGE;
						ctx->next_auth = ctx->auths;
					}
					goto nextreq;
				}

				/* if the challenge is not ready, wait to be wakeup */
				if (ctx->next_auth->ready != ctx->cfg->cond_ready)
					goto wait;

				if (acme_req_challenge(task, ctx, ctx->next_auth, &errmsg) != 0)
					goto retry;
			}
			if (http_st == ACME_HTTP_RES) {
				enum acme_ret ret = acme_res_challenge(task, ctx, ctx->next_auth, 0, &errmsg);

				if (ret == ACME_RET_RETRY) {
					goto retry;
				} else if (ret == ACME_RET_FAIL) {
					goto end;
				}
				if ((ctx->next_auth = ctx->next_auth->next) == NULL) {
					st = ACME_CHKCHALLENGE;
					ctx->next_auth = ctx->auths;
					/* let 5 seconds before checking the challenge */
					if (ctx->retryafter == 0)
						ctx->retryafter = 5;
				}
				/* call with next auth or do the challenge step */
				goto nextreq;
			}
		break;
		case ACME_CHKCHALLENGE:
			if (http_st == ACME_HTTP_REQ) {
				/* if challenge is already validated we skip this stage */
				if (ctx->next_auth->validated) {
					if ((ctx->next_auth = ctx->next_auth->next) == NULL)
						st = ACME_FINALIZE;

					goto nextreq;
				}

				if (acme_post_as_get(task, ctx, ctx->next_auth->chall, &errmsg) != 0)
					goto retry;
			}
			if (http_st == ACME_HTTP_RES) {
				enum acme_ret ret = acme_res_challenge(task, ctx, ctx->next_auth, 1, &errmsg);
				if (ret == ACME_RET_RETRY) {
					goto retry;
				} else if (ret == ACME_RET_FAIL) {
					goto abort;
				}
				if ((ctx->next_auth = ctx->next_auth->next) == NULL)
					st = ACME_FINALIZE;

				/* do it with the next auth or finalize */
				goto nextreq;
			}
		break;
		case ACME_FINALIZE:
			if (http_st == ACME_HTTP_REQ) {
				if (acme_req_finalize(task, ctx, &errmsg) != 0)
					goto retry;
			}
			if (http_st == ACME_HTTP_RES) {
				if (acme_res_finalize(task, ctx, &errmsg) != 0) {
					goto retry;
				}
				/* let 5 seconds to the server to generate the cert */
				if (ctx->retryafter == 0)
					ctx->retryafter = 5;
				st = ACME_CHKORDER;
				goto nextreq;
			}
		break;
		case ACME_CHKORDER:
			if (http_st == ACME_HTTP_REQ) {
				if (acme_post_as_get(task, ctx, ctx->order, &errmsg) != 0)
					goto retry;
			}
			if (http_st == ACME_HTTP_RES) {
				if (acme_res_chkorder(task, ctx, &errmsg) != 0) {
					goto retry;
				}
				st = ACME_CERTIFICATE;
				goto nextreq;
			}
		break;
		case ACME_CERTIFICATE:
			if (http_st == ACME_HTTP_REQ) {
				if (acme_post_as_get(task, ctx, ctx->certificate, &errmsg) != 0)
					goto retry;
			}
			if (http_st == ACME_HTTP_RES) {
				if (acme_res_certificate(task, ctx, &errmsg) != 0) {
					goto retry;
				}
				goto end;
			}
		break;

		case ACME_END:
			goto end;
		break;

	}

	/* this is called after initializing a request */
	ctx->http_state = http_st;
	ctx->state = st;
	task->expire = TICK_ETERNITY;
	return task;

nextreq:
	/* this is called when changing step in the state machine */
	http_st = ACME_HTTP_REQ;
	ctx->retries = ACME_RETRY; /* reinit the retries */
	ctx->http_state = http_st;
	ctx->state = st;

	if (ctx->retryafter == 0)
		goto re; /* optimize by not leaving the task for the next httpreq to init */

	/* if we have a retryafter, wait before next request (usually finalize) */
	task->expire = tick_add(now_ms, ctx->retryafter * 1000);
	ctx->retryafter = 0;

	return task;

retry:
	ctx->http_state = ACME_HTTP_REQ;
	ctx->state = st;

	ctx->retries--;
	if (ctx->retries > 0) {
		int delay = 1;
		int i;

		if (ctx->retryafter > 0) {
			/* Use the Retry-After value from the header */
			delay = ctx->retryafter;
			ctx->retryafter = 0;
		} else {
			/* else does an exponential backoff * 3 */
			for (i = 0; i < ACME_RETRY - ctx->retries; i++)
				delay *= 3;
		}
		send_log(NULL, LOG_NOTICE, "acme: %s: %s, retrying in %ds (%d/%d retries)...\n", ctx->store->path, errmsg ? errmsg : "", delay, ACME_RETRY - ctx->retries, ACME_RETRY);
		task->expire = tick_add(now_ms, delay * 1000);

	} else {
		send_log(NULL, LOG_NOTICE,"acme: %s: %s Aborting. (%d/%d)\n", ctx->store->path, errmsg ? errmsg : "", ACME_RETRY-ctx->retries, ACME_RETRY);
		goto end;
	}

	ha_free(&errmsg);

	return task;

abort:
	send_log(NULL, LOG_NOTICE,"acme: %s: %s Aborting.\n", ctx->store->path, errmsg ? errmsg : "");
	ha_free(&errmsg);

end:
	acme_del_acme_ctx_map(ctx);
	HA_RWLOCK_WRLOCK(OTHER_LOCK, &acme_lock);
	ebmb_delete(&ctx->node);
	HA_RWLOCK_WRUNLOCK(OTHER_LOCK, &acme_lock);
	acme_ctx_destroy(ctx);
	task_destroy(task);
	task = NULL;

	return task;

wait:
	/* wait for a task_wakeup */
	ctx->http_state = ACME_HTTP_REQ;
	ctx->state = st;
	task->expire = TICK_ETERNITY;
	return task;
}

/*
 * Return when the next task is scheduled
 * Check if the notAfter date will happen in (validity period / 12) or 7 days per default
 */
static time_t acme_schedule_date(struct ckch_store *store)
{
	time_t diff = 0;
	time_t notAfter = 0;
	time_t notBefore = 0;

	if (!global_ssl.acme_scheduler)
		return 0;

	/* compute the validity period of the leaf certificate */
	if (!store->data || !store->data->cert)
		return 0;

	notAfter = x509_get_notafter_time_t(store->data->cert);
	notBefore = x509_get_notbefore_time_t(store->data->cert);

	if ((notAfter >= 0 && notBefore >= 0)
	   && (notAfter > notBefore)) {
		diff = (notAfter - notBefore) / 12; /* validity period / 12 */
	} else {
		diff = 7 * 24 * 60 * 60; /* default to 7 days */
	}
	if (notAfter > diff) /* avoid overflow */
		return (notAfter - diff);
	else
		return 1; /* epoch+1 is long way expired */
}

/*
 * Return 1 if the certificate must be regenerated
 * Check if the notAfter date will append in (validity period / 12) or 7 days per default
 */
static int acme_will_expire(struct ckch_store *store)
{
	time_t notAfter = 0;

	/* compute the validity period of the leaf certificate */
	if (!store->data || !store->data->cert)
		return 0;

	notAfter = acme_schedule_date(store);

	if (notAfter <= date.tv_sec)
		return 1;

	return 0;
}

/* Does the scheduling of the ACME tasks
 */
struct task *acme_scheduler(struct task *task, void *context, unsigned int state)
{
	struct ebmb_node *node = NULL;
	struct ckch_store *store = NULL;
	char *errmsg = NULL;

	if (HA_SPIN_TRYLOCK(CKCH_LOCK, &ckch_lock))
		return task;

	node = ebmb_first(&ckchs_tree);
	while (node) {
		store = ebmb_entry(node, struct ckch_store, node);

		if (store->conf.acme.id) {

			if (acme_will_expire(store)) {
				TRACE_USER("ACME Scheduling start", ACME_EV_SCHED);
				if (acme_start_task(store, &errmsg) != 0) {
					send_log(NULL, LOG_NOTICE,"acme: %s: %s Aborting.\n", store->path, errmsg ? errmsg : "");
					ha_free(&errmsg);
				}
			}
		}
		node = ebmb_next(node);
	}
end:
	HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock);
	/* call the task again in 12h */
	/* XXX: need to be configured */
	task->expire = tick_add(now_ms, 12 * 60 * 60 * 1000);
	return task;
}

/*
 * Generate a X509_REQ using a PKEY and a list of SAN finished by a NULL entry
 */
X509_REQ *acme_x509_req(EVP_PKEY *pkey, char **san)
{
	struct buffer *san_trash = NULL;
	X509_REQ *x = NULL;
	X509_NAME *nm = NULL;
	STACK_OF(X509_EXTENSION) *exts = NULL;
	X509_EXTENSION *ext_san = NULL;
	char *str_san = NULL;
	int i = 0;

	if ((san_trash = alloc_trash_chunk()) == NULL)
		goto error;

	if ((x = X509_REQ_new()) == NULL)
		goto error;

	if (!X509_REQ_set_pubkey(x, pkey))
		goto error;

	if ((nm = X509_NAME_new()) == NULL)
		goto error;

	/* common name is the first SAN in the list */
	if (!X509_NAME_add_entry_by_txt(nm, "CN", MBSTRING_ASC,
	                         (unsigned char *)san[0], -1, -1, 0))
		goto error;
	/* assign the CN to the REQ */
	if (!X509_REQ_set_subject_name(x, nm))
		goto error;

	/* Add the SANs */
	if ((exts = sk_X509_EXTENSION_new_null()) == NULL)
		goto error;

	for (i = 0; san[i]; i++) {
		chunk_appendf(san_trash, "%sDNS:%s", i ? "," : "", san[i]);
	}
	if ((str_san = my_strndup(san_trash->area, san_trash->data)) == NULL)
		goto error;

	if ((ext_san = X509V3_EXT_conf_nid(NULL, NULL, NID_subject_alt_name, str_san)) == NULL)
		goto error;

	if (!sk_X509_EXTENSION_push(exts, ext_san))
		goto error;

	ext_san = NULL; /* handle double-free upon error */

	if (!X509_REQ_add_extensions(x, exts))
		goto error;

	if (!X509_REQ_sign(x, pkey, EVP_sha256()))
		goto error;

	sk_X509_EXTENSION_pop_free(exts, X509_EXTENSION_free);
	X509_NAME_free(nm);
	free(str_san);
	free_trash_chunk(san_trash);

	return x;

error:
	X509_EXTENSION_free(ext_san);
	sk_X509_EXTENSION_pop_free(exts, X509_EXTENSION_free);
	X509_REQ_free(x);
	X509_NAME_free(nm);
	free(str_san);
	free_trash_chunk(san_trash);
	return NULL;

}

/*
 * Generate a temporary expired X509  or reuse the one generated.
 * Use tmp_pkey to generate
 *
 * Increment the refcount when returning the existing one
 */
X509 *acme_gen_tmp_x509()
{
	if (tmp_x509) {
		X509_up_ref(tmp_x509);
		return tmp_x509;
	}

	if (!tmp_pkey)
		return NULL;

	tmp_x509 = ssl_gen_x509(tmp_pkey);

	return tmp_x509;
}

/*
 * Generate a temporary RSA2048 pkey or reuse the one generated.
 *
 * Increment the refcount when returning the existing one
 *
 */
EVP_PKEY *acme_gen_tmp_pkey()
{
	if (tmp_pkey) {
		EVP_PKEY_up_ref(tmp_pkey);
		return tmp_pkey;
	}

	tmp_pkey = ssl_gen_EVP_PKEY(EVP_PKEY_RSA, 0, 2048, NULL);

	return tmp_pkey;
}


/* start an ACME task */
static int acme_start_task(struct ckch_store *store, char **errmsg)
{
	struct task *task = NULL;
	struct acme_ctx *ctx = NULL;
	struct acme_cfg *cfg;
	struct ckch_store *newstore = NULL;
	EVP_PKEY *pkey = NULL;

	if (!store->conf.acme.domains) {
		memprintf(errmsg, "No 'domains' were configured for certificate. ");
		goto err;
	}

	cfg = get_acme_cfg(store->conf.acme.id);
	if (!cfg) {
		memprintf(errmsg, "No ACME configuration found for file '%s'.", store->path);
		goto err;
	}

	newstore = ckchs_dup(store);
	if (!newstore) {
		memprintf(errmsg, "Out of memory.");
		goto err;
	}

	task = task_new_anywhere();
	if (!task)
		goto err;
	task->nice = 0;
	task->process = acme_process;

	/* XXX: following init part could be done in the task */
	ctx = calloc(1, sizeof *ctx + strlen(newstore->path) + 1);
	if (!ctx) {
		memprintf(errmsg, "Out of memory.");
		goto err;
	}

	memcpy(ctx->name, newstore->path, strlen(newstore->path) + 1);

	HA_RWLOCK_WRLOCK(OTHER_LOCK, &acme_lock);
	if (ebst_insert(&acme_tasks, &ctx->node) != &ctx->node) {
		memprintf(errmsg, "%sTask already exists for '%s' certificate.\n", *errmsg ? *errmsg : "", newstore->path);
		HA_RWLOCK_WRUNLOCK(OTHER_LOCK, &acme_lock);
		goto err;
	}
	HA_RWLOCK_WRUNLOCK(OTHER_LOCK, &acme_lock);

	/* set the number of remaining retries when facing an error */
	ctx->retries = ACME_RETRY;

	if (!cfg->reuse_key) {
		if ((pkey = ssl_gen_EVP_PKEY(cfg->key.type, cfg->key.curves, cfg->key.bits, errmsg)) == NULL)
			goto err;

		EVP_PKEY_free(newstore->data->key);
		newstore->data->key = pkey;
		pkey = NULL;
	}

	ctx->req = acme_x509_req(newstore->data->key, store->conf.acme.domains);
	if (!ctx->req) {
		memprintf(errmsg, "%sCan't generate a CSR.", *errmsg ? *errmsg : "");
		goto err;
	}

	ctx->store = newstore;
	ctx->cfg = cfg;
	task->context = ctx;
	ctx->task = task;

	send_log(NULL, LOG_NOTICE, "acme: %s: Starting update of the certificate.\n", ctx->store->path);

	TRACE_USER("ACME Task start", ACME_EV_NEW, ctx);
	task_wakeup(task, TASK_WOKEN_INIT);

	return 0;

err:
	EVP_PKEY_free(pkey);
	ckch_store_free(newstore);
	if (ctx) {
		HA_RWLOCK_WRLOCK(OTHER_LOCK, &acme_lock);
		ebmb_delete(&ctx->node);
		HA_RWLOCK_WRUNLOCK(OTHER_LOCK, &acme_lock);
		acme_ctx_destroy(ctx);
	}
	if (task)
		task_destroy(task);
	memprintf(errmsg, "%sCan't start the ACME client.", *errmsg ? *errmsg : "");
	return 1;
}

static int cli_acme_renew_parse(char **args, char *payload, struct appctx *appctx, void *private)
{
	struct ckch_store *store = NULL;
	char *errmsg = NULL;

	if (!cli_has_level(appctx, ACCESS_LVL_ADMIN))
		return 1;

	if (!*args[2]) {
		memprintf(&errmsg, ": not enough parameters\n");
		goto err;
	}

	if (HA_SPIN_TRYLOCK(CKCH_LOCK, &ckch_lock))
		return cli_err(appctx, "Can't update: operations on certificates are currently locked!\n");

	if ((store = ckchs_lookup(args[2])) == NULL) {
		memprintf(&errmsg, "Can't find the certificate '%s'.\n", args[2]);
		goto err;
	}

	if (store->conf.acme.id == NULL) {
		memprintf(&errmsg, "No ACME configuration defined for file '%s'.\n", args[2]);
		goto err;
	}

	if (acme_start_task(store, &errmsg) != 0)
		goto err;

	HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock);
	return 0;
err:
	HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock);
	return cli_dynerr(appctx, errmsg);
}

static int cli_acme_chall_ready_parse(char **args, char *payload, struct appctx *appctx, void *private)
{
	char *msg = NULL;
	const char *crt;
	const char *dns;
	struct acme_ctx *ctx = NULL;
	struct acme_auth *auth = NULL;
	int found = 0;
	int remain = 0;
	struct ebmb_node *node = NULL;

	if (!cli_has_level(appctx, ACCESS_LVL_ADMIN))
		return 1;

	if (!*args[2] || !*args[3] || !*args[4]) {
		memprintf(&msg, "Not enough parameters: \"acme challenge_ready <certfile> domain <domain>\"\n");
		goto err;
	}

	crt = args[2];
	dns = args[4];

	HA_RWLOCK_WRLOCK(OTHER_LOCK, &acme_lock);
	node = ebst_lookup(&acme_tasks, crt);
	if (node) {
		ctx = ebmb_entry(node, struct acme_ctx, node);
		if (ctx->cfg->cond_ready & ACME_RDY_CLI)
			auth = ctx->auths;
		while (auth) {
			if (strncmp(dns, auth->dns.ptr, auth->dns.len) == 0) {
				if (!(auth->ready & ACME_RDY_CLI)) {
					auth->ready |= ACME_RDY_CLI;
					found++;
				} else {
					memprintf(&msg, "ACME challenge for crt \"%s\" and dns \"%s\" was already READY !\n", crt, dns);
				}
			}
			if ((auth->ready & ACME_RDY_CLI) == 0)
				remain++;
			auth = auth->next;
		}
	}
	HA_RWLOCK_WRUNLOCK(OTHER_LOCK, &acme_lock);
	if (!found) {
		if (!msg)
			memprintf(&msg, "Couldn't find an ACME task using crt \"%s\" and dns \"%s\" to set as ready!\n", crt, dns);
		goto err;
	} else {
		if (!remain) {
			if (ctx)
				task_wakeup(ctx->task, TASK_WOKEN_MSG);
			return cli_dynmsg(appctx, LOG_INFO, memprintf(&msg, "%d '%s' challenge(s) ready! All challenges ready, starting challenges validation!", found, dns));
		} else {
			return cli_dynmsg(appctx, LOG_INFO, memprintf(&msg, "%d '%s' challenge(s) ready! Remaining challenges to deploy: %d", found, dns, remain));
		}
	}

err:
	return cli_dynerr(appctx, msg);
}

static int cli_acme_status_io_handler(struct appctx *appctx)
{
	struct ebmb_node *node = NULL;
	struct ckch_store *store = NULL;

	if (HA_SPIN_TRYLOCK(CKCH_LOCK, &ckch_lock))
		return 1;

	chunk_reset(&trash);

	chunk_appendf(&trash, "# certificate\tsection\tstate\texpiration date (UTC)\texpires in\tscheduled date (UTC)\tscheduled in\n");
	if (applet_putchk(appctx, &trash) == -1)
		return 1;


	if (applet_putchk(appctx, &trash) == -1)
		return 1;

	/* TODO: handle backref list when list of task > buffer size */
	node = ebmb_first(&ckchs_tree);
	while (node) {
		store = ebmb_entry(node, struct ckch_store, node);

		if (store->conf.acme.id) {
			char str[50] = {};
			char *state;
			time_t notAfter = 0;
			time_t sched = 0;
			ullong remain = 0;
			int running = 0;

			HA_RWLOCK_RDLOCK(OTHER_LOCK, &acme_lock);
			running = !!ebst_lookup(&acme_tasks, store->path);
			HA_RWLOCK_RDUNLOCK(OTHER_LOCK, &acme_lock);

			if (global_ssl.acme_scheduler)
				state = "Scheduled";
			else
				state = "Stopped";

			if (running)
				state = "Running";

			chunk_appendf(&trash, "%s\t%s\t%s\t", store->path, store->conf.acme.id, state);

			notAfter = x509_get_notafter_time_t(store->data->cert);
			/* Expiration time */
			if (notAfter > date.tv_sec)
				remain = notAfter - date.tv_sec;
			strftime(str, sizeof(str), "%Y-%m-%dT%H:%M:%SZ", gmtime(&notAfter));
			chunk_appendf(&trash, "%s\t", str);
			chunk_appendf(&trash, "%llud %lluh%02llum%02llus\t", remain / 86400, (remain % 86400) / 3600, (remain % 3600) / 60, (remain % 60));

			/* Scheduled time */
			remain = 0;
			if (!running) /* if running no schedule date yet */
				sched = acme_schedule_date(store);
			if (sched > date.tv_sec)
				remain = sched - date.tv_sec;
			strftime(str, sizeof(str), "%Y-%m-%dT%H:%M:%SZ", gmtime(&sched));
			chunk_appendf(&trash, "%s\t", sched ? str : "-");
			if (sched)
				chunk_appendf(&trash, "%llud %lluh%02llum%02llus\n", remain / 86400, (remain % 86400) / 3600, (remain % 3600) / 60, (remain % 60));
			else
				chunk_appendf(&trash, "%s\n", "-");

			if (applet_putchk(appctx, &trash) == -1)
				return 1;
		}
		node = ebmb_next(node);
	}
end:
	HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock);
	return 1;
}

static int cli_acme_parse_status(char **args, char *payload, struct appctx *appctx, void *private)
{

	if (!cli_has_level(appctx, ACCESS_LVL_ADMIN))
		return 1;

	return 0;
}



static struct cli_kw_list cli_kws = {{ },{
	{ { "acme", "renew", NULL },           "acme renew <certfile>                   : renew a certificate using the ACME protocol", cli_acme_renew_parse, NULL, NULL, NULL, 0 },
	{ { "acme", "status", NULL },          "acme status                             : show status of certificates configured with ACME", cli_acme_parse_status, cli_acme_status_io_handler, NULL, NULL, 0 },
	{ { "acme", "challenge_ready", NULL }, "acme challenge_ready <certfile> domain <domain> : notify HAProxy that the ACME challenge is ready", cli_acme_chall_ready_parse, NULL, NULL, NULL, 0 },
	{ { NULL }, NULL, NULL, NULL }
}};

INITCALL1(STG_REGISTER, cli_register_kw, &cli_kws);

static void __acme_init(void)
{
	hap_register_feature("ACME");
	HA_RWLOCK_INIT(&acme_lock);
}
INITCALL0(STG_REGISTER, __acme_init);

#endif /* ! HAVE_ACME */

/*
 * Local variables:
 *  c-indent-level: 8
 *  c-basic-offset: 8
 * End:
 */
