MINOR: ssl: Add 'show ssl ocsp-updates' CLI command

This command can be used to dump information about the entries contained
in the ocsp update tree. It will display one line per concerned OCSP
response and will contain the expected next update time as well as the
time of the last successful update, and the number of successful and
failed attempts.
diff --git a/doc/management.txt b/doc/management.txt
index 7586aff..1fa7d6c 100644
--- a/doc/management.txt
+++ b/doc/management.txt
@@ -3468,6 +3468,37 @@
       Next Update: Oct 12 15:43:38 2048 GMT
       [...]
 
+show ssl ocsp-updates
+  Display information about the entries concerned by the OCSP update mechanism.
+  The command will output one line per OCSP response and will contain the
+  expected update time of the response as well as the time of the last
+  successful update and counters of successful and failed updates. It will also
+  give the status of the last update (successful or not) in numerical form as
+  well as text form. See below for a full list of possible errors. The lines
+  will be sorted by ascending 'Next Update' time. The lines will also contain a
+  path to the first frontend certificate that uses the OCSP response.
+  See "show ssl ocsp-response" command and "ocsp-update" option for more
+  information on the OCSP auto update.
+
+  The update error codes and error strings can be the following:
+
+  +----+-------------------------------------+
+  | ID | message                             |
+  +----+-------------------------------------+
+  |  0 | "Unknown"                           |
+  |  1 | "Update successful"                 |
+  |  2 | "HTTP error"                        |
+  |  3 | "Missing \"ocsp-response\" header"  |
+  |  4 | "OCSP response check failure"       |
+  |  5 | "Error during insertion"            |
+  +----+-------------------------------------+
+
+    Example :
+      $ echo "show ssl ocsp-updates" | socat /tmp/haproxy.sock -
+        OCSP Certid | Path | Next Update | Last Update | Successes | Failures | Last Update Status | Last Update Status (str)
+            303b300906052b0e03021a050004148a83e0060faff709ca7e9b95522a2e81635fda0a0414f652b0e435d5ea923851508f0adbe92d85de007a02021015 | /path_to_cert/cert.pem | 30/Jan/2023:00:08:09 +0000 | - | 0 | 1 | 2 | HTTP error
+            304b300906052b0e03021a0500041448dac9a0fb2bd32d4ff0de68d2f567b735f9b3c40414142eb317b75856cbae500940e61faf9d8b14c2c6021203e16a7aa01542f291237b454a627fdea9c1 | /path_to_cert/other_cert.pem | 30/Jan/2023:01:07:09 +0000 | 30/Jan/2023:00:07:09 +0000 | 1 | 0 | 1 | Update successful
+
 show ssl providers
   Display the names of the providers loaded by OpenSSL during init. Provider
   loading can indeed be configured via the OpenSSL configuration file and this
diff --git a/src/ssl_ocsp.c b/src/ssl_ocsp.c
index e234c7a..eaac5f7 100644
--- a/src/ssl_ocsp.c
+++ b/src/ssl_ocsp.c
@@ -1724,10 +1724,166 @@
 	return err_code;
 }
 
+struct show_ocsp_updates_ctx {
+	struct certificate_ocsp *cur_ocsp;
+};
+
+/*
+ * Parsing function for 'show ssl ocsp-updates [nb]'.
+ */
+static int cli_parse_show_ocsp_updates(char **args, char *payload, struct appctx *appctx, void *private)
+{
+#if ((defined SSL_CTRL_SET_TLSEXT_STATUS_REQ_CB && !defined OPENSSL_NO_OCSP) && !defined OPENSSL_IS_BORINGSSL)
+	struct show_ocsp_updates_ctx *ctx = applet_reserve_svcctx(appctx, sizeof(*ctx));
+
+	HA_SPIN_LOCK(OCSP_LOCK, &ocsp_tree_lock);
+
+	return 0;
+#else
+	return cli_err(appctx, "HAProxy was compiled against a version of OpenSSL that doesn't support OCSP stapling.\n");
+#endif
+}
+
+/*
+ * Dump information about an ocsp response concerning ocsp auto update.
+ * It follows the following format :
+ * OCSP Certid | Path | Next Update | Last Update | Successes | Failures | Last Update Status | Last Update Status (str)
+ * Return 0 in case of success.
+ */
+static int dump_ocsp_update_info(struct certificate_ocsp *ocsp, struct buffer *out)
+{
+	struct tm tm = {};
+	char *ret;
+	int i;
+	time_t next_update;
+
+	/* Dump OCSP certid */
+	for (i = 0; i < ocsp->key_length; ++i) {
+		chunk_appendf(out, "%02x", ocsp->key_data[i]);
+	}
+
+	chunk_appendf(out, " | ");
+
+	/* Dump path */
+	chunk_appendf(out, "%s", ocsp->path);
+
+	chunk_appendf(out, " | ");
+
+	/* Dump next update time */
+	if (ocsp->next_update.key != 0) {
+		next_update = ocsp->next_update.key;
+		get_localtime(ocsp->next_update.key, &tm);
+	} else {
+		next_update = date.tv_sec;
+		get_localtime(date.tv_sec, &tm);
+	}
+	ret = localdate2str_log(b_orig(out)+b_data(out), next_update, &tm, b_size(out)-b_data(out));
+
+	if (ret == NULL)
+		return 1;
+
+	out->data = (ret - out->area);
+
+	chunk_appendf(out, " | ");
+
+	/* Dump last update time or "-" if no update occurred yet */
+	if (ocsp->last_update) {
+		get_localtime(ocsp->last_update, &tm);
+		ret = localdate2str_log(b_orig(out)+b_data(out), ocsp->last_update, &tm, b_size(out)-b_data(out));
+
+		if (ret == NULL)
+			return 1;
+
+		out->data = (ret - out->area);
+	} else
+		chunk_appendf(out, "-");
+
+	chunk_appendf(out, " | ");
+
+	/* Number of successful updates */
+	chunk_appendf(out, "%d", ocsp->num_success);
+
+	chunk_appendf(out, " | ");
+
+	/* Number of failed updates */
+	chunk_appendf(out, "%d", ocsp->num_failure);
+
+	chunk_appendf(out, " | ");
+
+	/* Last update status */
+	chunk_appendf(out, "%d", ocsp->last_update_status);
+
+	chunk_appendf(out, " | ");
+
+	/* Last update status str */
+	if (ocsp->last_update_status >= OCSP_UPDT_ERR_LAST)
+		chunk_appendf(out, "-");
+	else
+		chunk_appendf(out, "%s", istptr(ocsp_update_errors[ocsp->last_update_status]));
+
+	chunk_appendf(out, "\n");
+
+	return 0;
+}
+
+static int cli_io_handler_show_ocsp_updates(struct appctx *appctx)
+{
+	struct show_ocsp_updates_ctx *ctx = appctx->svcctx;
+	struct eb64_node *node;
+	struct certificate_ocsp *ocsp = NULL;
+	struct buffer *trash = get_trash_chunk();
+
+	if (!ctx->cur_ocsp) {
+		node = eb64_first(&ocsp_update_tree);
+		chunk_appendf(trash, "OCSP Certid | Path | Next Update | Last Update | Successes | Failures | Last Update Status | Last Update Status (str)\n");
+
+		/* Look for an entry currently being updated */
+		ocsp = ssl_ocsp_task_ctx.cur_ocsp;
+		if (ocsp) {
+			if (dump_ocsp_update_info(ocsp, trash))
+				goto end;
+		}
+
+		if (applet_putchk(appctx, trash) == -1)
+			goto yield;
+
+	} else {
+		node = &((struct certificate_ocsp*)ctx->cur_ocsp)->next_update;
+	}
+
+	while (node) {
+		ocsp = eb64_entry(node, struct certificate_ocsp, next_update);
+
+		chunk_reset(trash);
+		if (dump_ocsp_update_info(ocsp, trash))
+			goto end;
+
+		if (applet_putchk(appctx, trash) == -1) {
+			ctx->cur_ocsp = ocsp;
+			goto yield;
+		}
+
+		node = eb64_next(node);
+	}
+
+end:
+	return 1;
+
+yield:
+	return 0; /* should come back */
+}
+
+static void cli_release_show_ocsp_updates(struct appctx *appctx)
+{
+	HA_SPIN_UNLOCK(OCSP_LOCK, &ocsp_tree_lock);
+}
+
+
 static struct cli_kw_list cli_kws = {{ },{
 	{ { "set", "ssl", "ocsp-response", NULL }, "set ssl ocsp-response <resp|payload>    : update a certificate's OCSP Response from a base64-encode DER",      cli_parse_set_ocspresponse, NULL },
 
 	{ { "show", "ssl", "ocsp-response", NULL },"show ssl ocsp-response [id]             : display the IDs of the OCSP responses used in memory, or the details of a single OCSP response", cli_parse_show_ocspresponse, cli_io_handler_show_ocspresponse, NULL },
+	{ { "show", "ssl", "ocsp-updates", NULL }, "show ssl ocsp-updates                   : display information about the next 'nb' ocsp responses that will be updated automatically", cli_parse_show_ocsp_updates, cli_io_handler_show_ocsp_updates, cli_release_show_ocsp_updates },
 #if ((defined SSL_CTRL_SET_TLSEXT_STATUS_REQ_CB && !defined OPENSSL_NO_OCSP) && !defined OPENSSL_IS_BORINGSSL)
 	{ { "update", "ssl", "ocsp-response", NULL }, "update ssl ocsp-response <certfile>  : send ocsp request and update stored ocsp response",                  cli_parse_update_ocsp_response, cli_io_handler_update_ocsp_response, cli_release_update_ocsp_response },
 #endif