Hello,

On Fri, Mar 23, 2018 at 09:43:50PM +0100, Aurélien Nephtali wrote:
> Hello,
> 
> This patch adds multi-line mode support to the CLI.

[...]

Please consider reviewing the attached patch instead as it fixes a
compiler warning.

-- 
Aurélien.
>From 53356f83dab6483512d2db1fae87abd24beca4c0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Aur=C3=A9lien=20Nephtali?= <aurelien.nepht...@corp.ovh.com>
Date: Thu, 15 Mar 2018 15:44:19 +0100
Subject: [PATCH] MEDIUM: cli: Add multi-line mode support
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Add a new command to toggle multi-line mode: multiline.
Once activated, all commands must end with an empty line.

The multi-line mode enables line feeds to be used anywhere in the
request as long it stays syntactically correct once reassembled.
This mode should be particularly useful for scripts or when inputing
data that can contain line feeds.

Examples:
$ echo -e "multiline;set ssl ocsp-response $(base64 ocsp.der)\n" | socat
/tmp/sock1 -
$ echo -e "multiline ; show\nstat\n" | socat /tmp/sock1 -

Signed-off-by: Aurélien Nephtali <aurelien.nepht...@corp.ovh.com>
---
 doc/management.txt     |  23 +++++++
 include/proto/applet.h |   4 +-
 include/types/applet.h |   5 +-
 src/cli.c              | 165 +++++++++++++++++++++++++++++++++++--------------
 src/ssl_sock.c         |  20 +++++-
 5 files changed, 168 insertions(+), 49 deletions(-)

diff --git a/doc/management.txt b/doc/management.txt
index 1488862e7..dd52cf82a 100644
--- a/doc/management.txt
+++ b/doc/management.txt
@@ -1563,6 +1563,29 @@ help
   Print the list of known keywords and their basic usage. The same help screen
   is also displayed for unknown commands.
 
+multiline
+  Toggle the multi-line mode. When enabled, this mode allows commands to be split
+  across multiple lines. To signal the end of a multi-line request, an empty
+  line must be sent. The multi-line mode can also be used with the interactive
+  mode: if 'prompt' was enabled beforehand, the CLI becomes interactive with
+  multi-line support. The prompt will also change from '> ' to '+ ' to emphasize
+  that an empty line is needed to finish and parse the command.
+
+  Example:
+    $ echo -e "multiline;set server\nTEST/A\nstate ready\n" | socat /tmp/sock1 -
+    $ socat /tmp/sock1 -
+    prompt
+
+    > multiline
+
+    > set
+    + server TEST/A
+    + state
+    + ready
+    +
+
+    >
+
 prompt
   Toggle the prompt at the beginning of the line and enter or leave interactive
   mode. In interactive mode, the connection is not closed after a command
diff --git a/include/proto/applet.h b/include/proto/applet.h
index cdd4d90f0..7cc2c0ad1 100644
--- a/include/proto/applet.h
+++ b/include/proto/applet.h
@@ -43,11 +43,13 @@ static int inline appctx_res_wakeup(struct appctx *appctx);
 
 /* Initializes all required fields for a new appctx. Note that it does the
  * minimum acceptable initialization for an appctx. This means only the
- * 3 integer states st0, st1, st2 are zeroed.
+ * 3 integer states st0, st1, st2 and the chunk used to gather unfinished
+ * commands are zeroed
  */
 static inline void appctx_init(struct appctx *appctx, unsigned long thread_mask)
 {
 	appctx->st0 = appctx->st1 = appctx->st2 = 0;
+	appctx->chunk = NULL;
 	appctx->io_release = NULL;
 	appctx->thread_mask = thread_mask;
 	appctx->state = APPLET_SLEEPING;
diff --git a/include/types/applet.h b/include/types/applet.h
index 89c318c1d..9789e202a 100644
--- a/include/types/applet.h
+++ b/include/types/applet.h
@@ -57,8 +57,11 @@ struct appctx {
 	/* 3 unused bytes here */
 	unsigned short state;      /* Internal appctx state */
 	unsigned int st0;          /* CLI state for stats, session state for peers */
-	unsigned int st1;          /* prompt for stats, session error for peers */
+	unsigned int st1;          /* prompt/multiline for stats, session error for peers */
+#define APPCTX_CLI_ST1_PROMPT    (1 << 0)
+#define APPCTX_CLI_ST1_MULTILINE (1 << 1)
 	unsigned int st2;          /* output state for stats, unused by peers  */
+	struct chunk *chunk;       /* used to store unfinished commands */
 	struct applet *applet;     /* applet this context refers to */
 	void *owner;               /* pointer to upper layer's entity (eg: stream interface) */
 	struct act_rule *rule;     /* rule associated with the applet. */
diff --git a/src/cli.c b/src/cli.c
index ada45a865..cc76a3127 100644
--- a/src/cli.c
+++ b/src/cli.c
@@ -73,6 +73,7 @@ static const char stats_sock_usage_msg[] =
 	"Unknown command. Please enter one of the following commands only :\n"
 	"  help           : this message\n"
 	"  prompt         : toggle interactive mode with prompt\n"
+	"  multiline      : toggle multi-line mode\n"
 	"  quit           : disconnect\n"
 	"";
 
@@ -90,7 +91,7 @@ static struct cli_kw_list cli_keywords = {
 
 extern const char *stat_status_codes[];
 
-char *cli_gen_usage_msg()
+static char *cli_gen_usage_msg(struct appctx *appctx)
 {
 	struct cli_kw_list *kw_list;
 	struct cli_kw *kw;
@@ -101,7 +102,7 @@ char *cli_gen_usage_msg()
 	dynamic_usage_msg = NULL;
 
 	if (LIST_ISEMPTY(&cli_keywords.list))
-		return NULL;
+		goto end;
 
 	chunk_reset(tmp);
 	chunk_strcat(tmp, stats_sock_usage_msg);
@@ -115,6 +116,18 @@ char *cli_gen_usage_msg()
 	chunk_init(&out, NULL, 0);
 	chunk_dup(&out, tmp);
 	dynamic_usage_msg = out.str;
+
+end:
+	if (dynamic_usage_msg) {
+		appctx->ctx.cli.severity = LOG_INFO;
+		appctx->ctx.cli.msg = dynamic_usage_msg;
+	}
+	else {
+		appctx->ctx.cli.severity = LOG_INFO;
+		appctx->ctx.cli.msg = stats_sock_usage_msg;
+	}
+	appctx->st0 = CLI_ST_PRINT;
+
 	return dynamic_usage_msg;
 }
 
@@ -373,7 +386,9 @@ static int cli_get_severity_output(struct appctx *appctx)
  * from the CLI's IO handler running in an appctx context. The function returns 1
  * if the request was understood, otherwise zero. It is called with appctx->st0
  * set to CLI_ST_GETREQ and presets ->st2 to 0 so that parsers don't have to do
- * it. It will possilbly leave st0 to CLI_ST_CALLBACK if the keyword needs to
+ * it. Keyword parsers are fed with an array of strings representing the arguments.
+ * The empty string marks the end of the array.
+ * It will possilbly leave st0 to CLI_ST_CALLBACK if the keyword needs to
  * have its own I/O handler called again. Most of the time, parsers will only
  * set st0 to CLI_ST_PRINT and put their message to be displayed into cli.msg.
  * If a keyword parser is NULL and an I/O handler is declared, the I/O handler
@@ -389,8 +404,13 @@ static int cli_parse_request(struct appctx *appctx, char *line)
 	while (isspace((unsigned char)*line))
 		line++;
 
+	/* force an empty string on the last element in case too many arguments
+	 * are received
+	 */
+	args[MAX_STATS_ARGS] = "";
+
 	arg = 0;
-	args[arg] = line;
+	args[arg++] = line;
 
 	while (*line && arg < MAX_STATS_ARGS) {
 		if (*line == '\\') {
@@ -404,19 +424,22 @@ static int cli_parse_request(struct appctx *appctx, char *line)
 			while (isspace((unsigned char)*line))
 				line++;
 
-			args[++arg] = line;
+			args[arg++] = line;
 			continue;
 		}
 
 		line++;
 	}
 
-	while (++arg <= MAX_STATS_ARGS)
-		args[arg] = line;
+	/* fill the array with empty strings (<line> points on the \0 of
+	 * the input string) or do nothing in case there are too many arguments
+	 */
+	while (arg < MAX_STATS_ARGS)
+		args[arg++] = line;
 
 	/* unescape '\' */
 	arg = 0;
-	while (arg <= MAX_STATS_ARGS && *args[arg] != '\0') {
+	while (*args[arg] != '\0') {
 		j = 0;
 		for (i=0; args[arg][i] != '\0'; i++) {
 			if (args[arg][i] == '\\') {
@@ -495,7 +518,7 @@ static void cli_io_handler(struct appctx *appctx)
 	struct channel *res = si_ic(si);
 	struct bind_conf *bind_conf = strm_li(si_strm(si))->bind_conf;
 	int reql;
-	int len;
+	int len = -1;
 
 	if (unlikely(si->state == SI_ST_DIS || si->state == SI_ST_CLO))
 		goto out;
@@ -518,6 +541,7 @@ static void cli_io_handler(struct appctx *appctx)
 			/* Let's close for real now. We just close the request
 			 * side, the conditions below will complete if needed.
 			 */
+			free_trash_chunk(appctx->chunk);
 			si_shutw(si);
 			break;
 		}
@@ -568,46 +592,59 @@ static void cli_io_handler(struct appctx *appctx)
 
 			trash.str[len] = '\0';
 
-			appctx->st0 = CLI_ST_PROMPT;
-			if (len) {
-				if (strcmp(trash.str, "quit") == 0) {
+			/* use a trash chunk to store received data */
+			if (!appctx->chunk) {
+				appctx->chunk = alloc_trash_chunk();
+				if (!appctx->chunk) {
 					appctx->st0 = CLI_ST_END;
 					continue;
 				}
-				else if (strcmp(trash.str, "prompt") == 0)
-					appctx->st1 = !appctx->st1;
-				else if (strcmp(trash.str, "help") == 0 ||
-					 !cli_parse_request(appctx, trash.str)) {
-					cli_gen_usage_msg();
-					if (dynamic_usage_msg) {
-						appctx->ctx.cli.severity = LOG_INFO;
-						appctx->ctx.cli.msg = dynamic_usage_msg;
-					}
-					else {
-						appctx->ctx.cli.severity = LOG_INFO;
-						appctx->ctx.cli.msg = stats_sock_usage_msg;
-					}
-					appctx->st0 = CLI_ST_PRINT;
-				}
-				/* NB: stats_sock_parse_request() may have put
-				 * another CLI_ST_O_* into appctx->st0.
-				 */
 			}
-			else if (!appctx->st1) {
-				/* if prompt is disabled, print help on empty lines,
-				 * so that the user at least knows how to enable
-				 * prompt and find help.
-				 */
-				cli_gen_usage_msg();
-				if (dynamic_usage_msg) {
-					appctx->ctx.cli.severity = LOG_INFO;
-					appctx->ctx.cli.msg = dynamic_usage_msg;
+
+			/* special case, exit immediately */
+			if (strcmp(trash.str, "quit") == 0) {
+				appctx->st0 = CLI_ST_END;
+				continue;
+			}
+
+			chunk_appendf(appctx->chunk, "%s", trash.str);
+
+			appctx->st0 = CLI_ST_PROMPT;
+
+			/* multi-line mode is enabled */
+			if (appctx->st1 & APPCTX_CLI_ST1_MULTILINE) {
+				if (!len) {
+					/* empty line */
+					if (!cli_parse_request(appctx, appctx->chunk->str))
+						cli_gen_usage_msg(appctx);
+
+					chunk_reset(appctx->chunk);
 				}
 				else {
-					appctx->ctx.cli.severity = LOG_INFO;
-					appctx->ctx.cli.msg = stats_sock_usage_msg;
+					chunk_appendf(appctx->chunk, "\n");
+				}
+			}
+			else {
+				/* multi-line mode is disabled */
+				if (len) {
+					if (!cli_parse_request(appctx, appctx->chunk->str))
+						cli_gen_usage_msg(appctx);
+
+					chunk_reset(appctx->chunk);
+
+					/* multi-line mode was just activated, force
+					 * an empty line to correctly output a LF
+					 */
+					if (appctx->st1 & APPCTX_CLI_ST1_MULTILINE)
+						len = 0;
+				}
+				else if (!(appctx->st1 & APPCTX_CLI_ST1_PROMPT)) {
+					/* if prompt is disabled, print help on empty lines,
+					 * so that the user at least knows how to enable
+					 * prompt and find help.
+					 */
+					cli_gen_usage_msg(appctx);
 				}
-				appctx->st0 = CLI_ST_PRINT;
 			}
 
 			/* re-adjust req buffer */
@@ -648,9 +685,29 @@ static void cli_io_handler(struct appctx *appctx)
 				break;
 			}
 
-			/* The post-command prompt is either LF alone or LF + '> ' in interactive mode */
 			if (appctx->st0 == CLI_ST_PROMPT) {
-				if (ci_putstr(si_ic(si), appctx->st1 ? "\n> " : "\n") != -1)
+				const char *prompt = "";
+
+				if (appctx->st1 & APPCTX_CLI_ST1_PROMPT) {
+					/* in multi-line and interactive mode, change the prompt
+					 * to emphasize that more data can still be sent
+					 */
+					if (appctx->chunk->len && appctx->st1 & APPCTX_CLI_ST1_MULTILINE)
+						prompt = "+ ";
+					else
+						prompt = "\n> ";
+				}
+				else {
+					if (len == 0 && appctx->st1 & APPCTX_CLI_ST1_MULTILINE) {
+						/* end of a multi-line command */
+						prompt = "\n";
+					}
+					else if (len > 0 && !(appctx->st1 & APPCTX_CLI_ST1_MULTILINE)) {
+						/* unfinished multi-line command */
+						prompt = "\n";
+					}
+				}
+				if (ci_putstr(si_ic(si), prompt) != -1)
 					appctx->st0 = CLI_ST_GETREQ;
 				else
 					si_applet_cant_put(si);
@@ -665,7 +722,7 @@ static void cli_io_handler(struct appctx *appctx)
 			 * buffer is empty. This still allows pipelined requests
 			 * to be sent in non-interactive mode.
 			 */
-			if ((res->flags & (CF_SHUTW|CF_SHUTW_NOW)) || (!appctx->st1 && !req->buf->o)) {
+			if ((res->flags & (CF_SHUTW|CF_SHUTW_NOW)) || (!(appctx->st1 & APPCTX_CLI_ST1_PROMPT) && !req->buf->o)) {
 				appctx->st0 = CLI_ST_END;
 				continue;
 			}
@@ -1454,6 +1511,21 @@ out:
 	return 1;
 }
 
+static int cli_parse_simple(char **args, struct appctx *appctx, void *private)
+{
+	if (**args == 'h')
+		/* help */
+		cli_gen_usage_msg(appctx);
+	else if (**args == 'm')
+		/* multiline */
+		appctx->st1 ^= APPCTX_CLI_ST1_MULTILINE;
+	else if (**args == 'p')
+		/* prompt */
+		appctx->st1 ^= APPCTX_CLI_ST1_PROMPT;
+
+	return 1;
+}
+
 
 
 static struct applet cli_applet = {
@@ -1473,6 +1545,9 @@ static struct cli_kw_list cli_kws = {{ },{
 	{ { "show", "cli", "sockets",  NULL }, "show cli sockets : dump list of cli sockets", cli_parse_default, cli_io_handler_show_cli_sock, NULL },
 	{ { "show", "fd", NULL }, "show fd [num] : dump list of file descriptors in use", cli_parse_show_fd, cli_io_handler_show_fd, NULL },
 	{ { "show", "activity", NULL }, "show activity : show per-thread activity stats (for support/developers)", cli_parse_default, cli_io_handler_show_activity, NULL },
+	{ { "help", NULL }, NULL, cli_parse_simple, NULL },
+	{ { "multiline", NULL }, NULL, cli_parse_simple, NULL },
+	{ { "prompt", NULL }, NULL, cli_parse_simple, NULL },
 	{ { "_getsocks", NULL }, NULL,  _getsocks, NULL },
 	{{},}
 }};
diff --git a/src/ssl_sock.c b/src/ssl_sock.c
index 8151cb381..017240870 100644
--- a/src/ssl_sock.c
+++ b/src/ssl_sock.c
@@ -8565,8 +8565,12 @@ static int cli_parse_set_ocspresponse(char **args, struct appctx *appctx, void *
 {
 #if (defined SSL_CTRL_SET_TLSEXT_STATUS_REQ_CB && !defined OPENSSL_NO_OCSP)
 	char *err = NULL;
+	struct chunk *chunk;
+	int i;
 
-	/* Expect one parameter: the new response in base64 encoding */
+	/* Expect at least one parameter: the new response in base64 encoding,
+	 * each extra parameter being part of the remainder of the response
+	 */
 	if (!*args[3]) {
 		appctx->ctx.cli.severity = LOG_ERR;
 		appctx->ctx.cli.msg = "'set ssl ocsp-response' expects response in base64 encoding.\n";
@@ -8574,7 +8578,19 @@ static int cli_parse_set_ocspresponse(char **args, struct appctx *appctx, void *
 		return 1;
 	}
 
-	trash.len = base64dec(args[3], strlen(args[3]), trash.str, trash.size);
+	chunk = alloc_trash_chunk();
+	if (!chunk) {
+		appctx->ctx.cli.severity = LOG_ERR;
+		appctx->ctx.cli.msg = "Not enough memory.\n";
+		appctx->st0 = CLI_ST_PRINT;
+		return 1;
+	}
+
+	for (i = 3; *args[i]; i++)
+		chunk_appendf(chunk, "%s", args[i]);
+
+	trash.len = base64dec(chunk->str, chunk->len, trash.str, trash.size);
+	free_trash_chunk(chunk);
 	if (trash.len < 0) {
 		appctx->ctx.cli.severity = LOG_ERR;
 		appctx->ctx.cli.msg = "'set ssl ocsp-response' received invalid base64 encoded response.\n";
-- 
2.11.0

Reply via email to