From 6a8099b071f29ebaaf5049e1514451b2bf9fc3e0 Mon Sep 17 00:00:00 2001
From: Andy Green <andy.green@linaro.org>
Date: Sun, 21 Feb 2016 21:25:48 +0800
Subject: [PATCH] cgi

Signed-off-by: Andy Green <andy.green@linaro.org>
---
 CMakeLists.txt                 |   9 +
 changelog                      |  25 +++
 lib/handshake.c                |  48 +++--
 lib/libwebsockets.c            | 320 +++++++++++++++++++++++++++++++++
 lib/libwebsockets.h            |  31 ++++
 lib/private-libwebsockets.h    |  36 ++++
 lib/server.c                   |   6 +-
 lib/service.c                  |  44 ++++-
 lws_config.h.in                |   3 +
 test-server/lws-cgi-test.sh    |  16 ++
 test-server/test-server-http.c | 112 +++++++++++-
 test-server/test-server.h      |   4 +
 12 files changed, 633 insertions(+), 21 deletions(-)
 create mode 100755 test-server/lws-cgi-test.sh

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8efdb080..02daa610 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -87,6 +87,7 @@ option(LWS_IPV6 "Compile with support for ipv6" OFF)
 option(LWS_WITH_HTTP2 "Compile with support for http2" OFF)
 option(LWS_MBED3 "Platform is MBED3" OFF)
 option(LWS_SSL_SERVER_WITH_ECDH_CERT "Include SSL server use ECDH certificate" OFF)
+option(LWS_WITH_CGI "Include CGI (spawn process with network-connected stdin/out/err) APIs" OFF)
 
 if (DEFINED YOTTA_WEBSOCKETS_VERSION_STRING)
 
@@ -1152,6 +1153,13 @@ if (NOT LWS_WITHOUT_TESTAPPS AND NOT LWS_WITHOUT_SERVER)
 	install(FILES ${TEST_SERVER_DATA}
 			DESTINATION share/libwebsockets-test-server
 			COMPONENT examples)
+if (LWS_WITH_CGI)
+	set(CGI_TEST_SCRIPT "${PROJECT_SOURCE_DIR}/test-server/lws-cgi-test.sh")
+	install(FILES ${CGI_TEST_SCRIPT}
+			PERMISSIONS  OWNER_EXECUTE GROUP_EXECUTE WORLD_EXECUTE OWNER_READ GROUP_READ WORLD_READ
+			DESTINATION share/libwebsockets-test-server
+			COMPONENT examples)
+	endif()
 endif()
 
 # Install the LibwebsocketsConfig.cmake and LibwebsocketsConfigVersion.cmake
@@ -1205,6 +1213,7 @@ message(" LWS_WITH_HTTP2 = ${LWS_WITH_HTTP2}")
 message(" LWS_MBED3 = ${LWS_MBED3}")
 message(" LWS_SSL_SERVER_WITH_ECDH_CERT = ${LWS_SSL_SERVER_WITH_ECDH_CERT}")
 message(" LWS_MAX_SMP = ${LWS_MAX_SMP}")
+message(" LWS_WITH_CGI = ${LWS_WITH_CGI}")
 message("---------------------------------------------------------------------")
 
 # These will be available to parent projects including libwebsockets using add_subdirectory()
diff --git a/changelog b/changelog
index 006cc20b..9a804c16 100644
--- a/changelog
+++ b/changelog
@@ -77,6 +77,31 @@ LWS_VISIBLE LWS_EXTERN struct lws *
 lws_adopt_socket_readbuf(struct lws_context *context, lws_sockfd_type accept_fd,
 		const char *readbuf, size_t len);
 
+3) MINOR NEWAPI CGI type "network io" subprocess execution is now possible from
+a simple api.
+
+LWS_VISIBLE LWS_EXTERN int
+lws_cgi(struct lws *wsi, char * const *exec_array, int timeout_secs);
+
+LWS_VISIBLE LWS_EXTERN int
+lws_cgi_kill(struct lws *wsi);
+
+To use it, you must first set the cmake option
+
+$ cmake .. -DLWS_WITH_CGI=1
+
+See test-server-http.c and  test server path
+
+http://localhost:7681/cgitest
+
+stdin gets http body, you can test it with wget
+
+$ echo hello > hello.txt
+$ wget http://localhost:7681/cgitest --post-file=hello.txt -O- --quiet
+lwstest script
+read="hello"
+
+
 
 v1.7.0
 ======
diff --git a/lib/handshake.c b/lib/handshake.c
index e48af843..6148ba54 100644
--- a/lib/handshake.c
+++ b/lib/handshake.c
@@ -156,14 +156,35 @@ http_postbody:
 			body_chunk_len = min(wsi->u.http.content_remain,len);
 			wsi->u.http.content_remain -= body_chunk_len;
 			len -= body_chunk_len;
+#ifdef LWS_WITH_CGI
+			if (wsi->cgi) {
+				struct lws_cgi_args args;
 
-			n = wsi->protocol->callback(wsi,
-				LWS_CALLBACK_HTTP_BODY, wsi->user_space,
-				buf, body_chunk_len);
-			if (n)
-				goto bail;
+				args.ch = LWS_STDIN;
+				args.stdwsi = &wsi->cgi->stdwsi[0];
+				args.data = buf;
+				args.len = body_chunk_len;
 
-			buf += body_chunk_len;
+				/* returns how much used */
+				n = user_callback_handle_rxflow(
+					wsi->protocol->callback,
+					wsi, LWS_CALLBACK_CGI_STDIN_DATA,
+					wsi->user_space,
+					(void *)&args, 0);
+				if (n < 0)
+					goto bail;
+			} else {
+#endif
+				n = wsi->protocol->callback(wsi,
+					LWS_CALLBACK_HTTP_BODY, wsi->user_space,
+					buf, body_chunk_len);
+				if (n)
+					goto bail;
+				n = body_chunk_len;
+#ifdef LWS_WITH_CGI
+			}
+#endif
+			buf += n;
 
 			if (wsi->u.http.content_remain)  {
 				lws_set_timeout(wsi, PENDING_TIMEOUT_HTTP_CONTENT,
@@ -173,11 +194,16 @@ http_postbody:
 			/* he sent all the content in time */
 postbody_completion:
 			lws_set_timeout(wsi, NO_PENDING_TIMEOUT, 0);
-			n = wsi->protocol->callback(wsi,
-				LWS_CALLBACK_HTTP_BODY_COMPLETION,
-				wsi->user_space, NULL, 0);
-			if (n)
-				goto bail;
+#ifdef LWS_WITH_CGI
+			if (!wsi->cgi)
+#endif
+			{
+				n = wsi->protocol->callback(wsi,
+					LWS_CALLBACK_HTTP_BODY_COMPLETION,
+					wsi->user_space, NULL, 0);
+				if (n)
+					goto bail;
+			}
 
 			goto http_complete;
 		}
diff --git a/lib/libwebsockets.c b/lib/libwebsockets.c
index b4bac9f3..e10250f7 100644
--- a/lib/libwebsockets.c
+++ b/lib/libwebsockets.c
@@ -20,6 +20,11 @@
  */
 
 #include "private-libwebsockets.h"
+#include <sys/types.h>
+#if defined(WIN32) || defined(_WIN32)
+#else
+#include <sys/wait.h>
+#endif
 
 int log_level = LLL_ERR | LLL_WARN | LLL_NOTICE;
 static void (*lwsl_emit)(int level, const char *line) = lwsl_emit_stderr;
@@ -112,6 +117,7 @@ lws_set_timeout(struct lws *wsi, enum pending_timeout reason, int secs)
 		*wsi->timeout_list_prev = wsi;
 	}
 
+	lwsl_debug("%s: %p: %d secs\n", __func__, wsi, secs);
 	wsi->pending_timeout_limit = now + secs;
 	wsi->pending_timeout = reason;
 
@@ -135,6 +141,29 @@ lws_close_free_wsi(struct lws *wsi, enum lws_close_status reason)
 	context = wsi->context;
 	pt = &context->pt[(int)wsi->tsi];
 
+#ifdef LWS_WITH_CGI
+	if (wsi->mode == LWSCM_CGI) {
+		/* we are not a network connection, but a handler for CGI io */
+		assert(wsi->master);
+		assert(wsi->master->cgi);
+		assert(wsi->master->cgi->stdwsi[(int)wsi->cgi_channel] == wsi);
+		/* end the binding between us and master */
+		wsi->master->cgi->stdwsi[(int)wsi->cgi_channel] = NULL;
+		wsi->master = NULL;
+		wsi->socket_is_permanently_unusable = 1;
+
+		goto just_kill_connection;
+	}
+
+	if (wsi->cgi) {
+		/* we have a cgi going, we must kill it and close the
+		 * related stdin/out/err wsis first
+		 */
+		wsi->cgi->being_closed = 1;
+		lws_cgi_kill(wsi);
+	}
+#endif
+
 	if (wsi->mode == LWSCM_HTTP_SERVING_ACCEPTED &&
 	    wsi->u.http.fd != LWS_INVALID_FILE) {
 		lwsl_debug("closing http file\n");
@@ -1305,3 +1334,294 @@ lws_extension_callback_pm_deflate(struct lws_context *context,
 }
 #endif
 
+LWS_VISIBLE LWS_EXTERN int
+lws_is_cgi(struct lws *wsi) {
+#ifdef LWS_WITH_CGI
+	return !!wsi->cgi;
+#else
+	return 0;
+#endif
+}
+
+#ifdef LWS_WITH_CGI
+
+static struct lws *
+lws_create_basic_wsi(struct lws_context *context, int tsi)
+{
+	struct lws *new_wsi;
+
+	if ((unsigned int)context->pt[tsi].fds_count ==
+	    context->fd_limit_per_thread - 1) {
+		lwsl_err("no space for new conn\n");
+		return NULL;
+	}
+
+	new_wsi = lws_zalloc(sizeof(struct lws));
+	if (new_wsi == NULL) {
+		lwsl_err("Out of memory for new connection\n");
+		return NULL;
+	}
+
+	new_wsi->tsi = tsi;
+	new_wsi->context = context;
+	new_wsi->pending_timeout = NO_PENDING_TIMEOUT;
+	new_wsi->rxflow_change_to = LWS_RXFLOW_ALLOW;
+
+	/* intialize the instance struct */
+
+	new_wsi->state = LWSS_CGI;
+	new_wsi->mode = LWSCM_CGI;
+	new_wsi->hdr_parsing_completed = 0;
+	new_wsi->position_in_fds_table = -1;
+
+	/*
+	 * these can only be set once the protocol is known
+	 * we set an unestablished connection's protocol pointer
+	 * to the start of the supported list, so it can look
+	 * for matching ones during the handshake
+	 */
+	new_wsi->protocol = context->protocols;
+	new_wsi->user_space = NULL;
+	new_wsi->ietf_spec_revision = 0;
+	new_wsi->sock = LWS_SOCK_INVALID;
+	context->count_wsi_allocated++;
+
+	return new_wsi;
+}
+
+/**
+ * lws_cgi: spawn network-connected cgi process
+ *
+ * @wsi: connection to own the process
+ * @exec_array: array of "exec-name" "arg1" ... "argn" NULL
+ */
+
+LWS_VISIBLE LWS_EXTERN int
+lws_cgi(struct lws *wsi, char * const *exec_array, int timeout_secs)
+{
+	struct lws_context_per_thread *pt = &wsi->context->pt[(int)wsi->tsi];
+	char *env_array[30], cgi_path[400];
+	struct lws_cgi *cgi;
+	int n;
+
+	/*
+	 * give the master wsi a cgi struct
+	 */
+
+	wsi->cgi = lws_zalloc(sizeof(*wsi->cgi));
+	if (!wsi->cgi) {
+		lwsl_err("%s: OOM\n", __func__);
+		return -1;
+	}
+
+	cgi = wsi->cgi;
+	cgi->wsi = wsi; /* set cgi's owning wsi */
+
+	/* create pipes for [stdin|stdout] and [stderr] */
+
+	for (n = 0; n < 3; n++)
+		if (pipe(cgi->pipe_fds[n]) == -1)
+			goto bail1;
+
+	/* create cgi wsis for each stdin/out/err fd */
+
+	for (n = 0; n < 3; n++) {
+		cgi->stdwsi[n] = lws_create_basic_wsi(wsi->context, wsi->tsi);
+		if (!cgi->stdwsi[n])
+			goto bail2;
+		cgi->stdwsi[n]->cgi_channel = n;
+		/* read side is 0, stdin we want the write side, others read */
+		cgi->stdwsi[n]->sock = cgi->pipe_fds[n][!!(n == 0)];
+		fcntl(cgi->pipe_fds[n][!!(n == 0)], F_SETFL, O_NONBLOCK);
+	}
+
+	for (n = 0; n < 3; n++) {
+		cgi->stdwsi[n]->master = wsi;
+		if (insert_wsi_socket_into_fds(wsi->context, cgi->stdwsi[n]))
+			goto bail3;
+	}
+
+	lws_change_pollfd(cgi->stdwsi[LWS_STDIN], LWS_POLLIN, LWS_POLLOUT);
+	lws_change_pollfd(cgi->stdwsi[LWS_STDOUT], LWS_POLLOUT, LWS_POLLIN);
+	lws_change_pollfd(cgi->stdwsi[LWS_STDERR], LWS_POLLOUT, LWS_POLLIN);
+
+	lwsl_debug("%s: fds in %d, out %d, err %d\n", __func__,
+			cgi->stdwsi[LWS_STDIN]->sock,
+			cgi->stdwsi[LWS_STDOUT]->sock,
+			cgi->stdwsi[LWS_STDERR]->sock);
+
+	lws_set_timeout(wsi, PENDING_TIMEOUT_CGI, timeout_secs);
+
+	/* add us to the pt list of active cgis */
+	cgi->cgi_list = pt->cgi_list;
+	pt->cgi_list = cgi;
+
+	/* prepare his CGI env */
+
+	n = 0;
+	if (wsi->u.hdr.ah) {
+		snprintf(cgi_path, sizeof(cgi_path) - 1, "PATH_INFO=%s",
+			 lws_hdr_simple_ptr(wsi, WSI_TOKEN_GET_URI));
+		cgi_path[sizeof(cgi_path) - 1] = '\0';
+		env_array[n++] = cgi_path;
+		if (lws_hdr_total_length(wsi, WSI_TOKEN_POST_URI))
+			env_array[n++] = "REQUEST_METHOD=POST";
+		else
+			env_array[n++] = "REQUEST_METHOD=GET";
+	}
+	if (lws_is_ssl(wsi))
+		env_array[n++] = "HTTPS=ON";
+	env_array[n++] = "SERVER_SOFTWARE=libwebsockets";
+	env_array[n++] = "PATH=/bin:/usr/bin:/usrlocal/bin";
+	env_array[n] = NULL;
+
+	/* we are ready with the redirection pipes... run the thing */
+#ifdef LWS_HAVE_VFORK
+	cgi->pid = vfork();
+#else
+	cgi->pid = fork();
+#endif
+	if (cgi->pid < 0) {
+		lwsl_err("fork failed, errno %d", errno);
+		goto bail3;
+	}
+
+	if (cgi->pid)
+		/* we are the parent process */
+		return 0;
+
+	/* We are the forked process, redirect and kill inherited things.
+	 *
+	 * Because of vfork(), we cannot do anything that changes pages in
+	 * the parent environment.  Stuff that changes kernel state for the
+	 * process is OK.  Stuff that happens after the execvpe() is OK.
+	 */
+
+	for (n = 0; n < 3; n++) {
+		if (dup2(cgi->pipe_fds[n][!(n == 0)], n) < 0) {
+			lwsl_err("%s: stdin dup2 failed\n", __func__);
+			goto bail3;
+		}
+		close(cgi->pipe_fds[n][!(n == 0)]);
+	}
+
+	execvpe(exec_array[0], &exec_array[0], &env_array[0]);
+	exit(1);
+
+bail3:
+	/* drop us from the pt cgi list */
+	pt->cgi_list = cgi->cgi_list;
+
+	while (--n >= 0)
+		remove_wsi_socket_from_fds(wsi->cgi->stdwsi[n]);
+bail2:
+	for (n = 0; n < 3; n++)
+		if (wsi->cgi->stdwsi[n])
+			lws_free_wsi(cgi->stdwsi[n]);
+
+bail1:
+	for (n = 0; n < 3; n++) {
+		if (cgi->pipe_fds[n][0])
+			close(cgi->pipe_fds[n][0]);
+		if (cgi->pipe_fds[n][1])
+			close(cgi->pipe_fds[n][1]);
+	}
+
+	lws_free_set_NULL(wsi->cgi);
+
+	lwsl_err("%s: failed\n", __func__);
+
+	return -1;
+}
+
+/**
+ * lws_cgi_kill: terminate cgi process associated with wsi
+ *
+ * @wsi: connection to own the process
+ */
+LWS_VISIBLE LWS_EXTERN int
+lws_cgi_kill(struct lws *wsi)
+{
+	struct lws_context_per_thread *pt = &wsi->context->pt[(int)wsi->tsi];
+	struct lws_cgi **pcgi = &pt->cgi_list;
+	struct lws_cgi_args args;
+	int n, status, do_close = 0;
+
+	if (!wsi->cgi)
+		return 0;
+
+	lwsl_notice("%s: wsi %p\n", __func__, wsi);
+
+	assert(wsi->cgi);
+
+	if (wsi->cgi->pid > 0) {
+		/* kill the process */
+		n = kill(wsi->cgi->pid, SIGTERM);
+		if (n < 0) {
+			lwsl_err("%s: failed\n", __func__);
+			return 1;
+		}
+		waitpid(wsi->cgi->pid, &status, 0); /* !!! may hang !!! */
+	}
+
+	args.stdwsi = &wsi->cgi->stdwsi[0];
+
+	if (wsi->cgi->pid != -1 && user_callback_handle_rxflow(
+			wsi->protocol->callback,
+			wsi, LWS_CALLBACK_CGI_TERMINATED,
+			wsi->user_space,
+			(void *)&args, 0)) {
+		wsi->cgi->pid = -1;
+		do_close = !wsi->cgi->being_closed;
+	}
+
+	/* remove us from the cgi list */
+	while (*pcgi) {
+		if (*pcgi == wsi->cgi) {
+			/* drop us from the pt cgi list */
+			*pcgi = (*pcgi)->cgi_list;
+			break;
+		}
+		pcgi = &(*pcgi)->cgi_list;
+	}
+
+	for (n = 0 ; n < 3; n++) {
+		if (wsi->cgi->pipe_fds[n][!!(n == 0)] >= 0) {
+			close(wsi->cgi->pipe_fds[n][!!(n == 0)]);
+			wsi->cgi->pipe_fds[n][!!(n == 0)] = -1;
+
+			lws_close_free_wsi(wsi->cgi->stdwsi[n], 0);
+		}
+	}
+
+	lws_free_set_NULL(wsi->cgi);
+
+	if (do_close)
+		lws_close_free_wsi(wsi, 0);
+
+	return 0;
+}
+
+LWS_EXTERN int
+lws_cgi_kill_terminated(struct lws_context_per_thread *pt)
+{
+	struct lws_cgi **pcgi = &pt->cgi_list, *cgi;
+	int status;
+
+	/* check all the subprocesses on the cgi list for termination */
+	while (*pcgi) {
+		/* get the next one because we may close current one next */
+		cgi = *pcgi;
+		pcgi = &(*pcgi)->cgi_list;
+
+		if (cgi->pid > 0 &&
+		    waitpid(cgi->pid, &status, WNOHANG) > 0) {
+			cgi->pid = 0;
+			lws_cgi_kill(cgi->wsi);
+			pcgi = &pt->cgi_list;
+		}
+	}
+
+	return 0;
+}
+#endif
diff --git a/lib/libwebsockets.h b/lib/libwebsockets.h
index 3e949392..5055c2f4 100644
--- a/lib/libwebsockets.h
+++ b/lib/libwebsockets.h
@@ -339,6 +339,11 @@ enum lws_callback_reasons {
 
 	LWS_CALLBACK_WS_EXT_DEFAULTS				= 39,
 
+	LWS_CALLBACK_CGI					= 40,
+	LWS_CALLBACK_CGI_TERMINATED				= 41,
+	LWS_CALLBACK_CGI_STDIN_DATA				= 42,
+	LWS_CALLBACK_CGI_STDIN_COMPLETED			= 43,
+
 	/****** add new things just above ---^ ******/
 
 	LWS_CALLBACK_USER = 1000, /* user code can use any including / above */
@@ -1535,6 +1540,7 @@ enum pending_timeout {
 	PENDING_TIMEOUT_AWAITING_CLIENT_HS_SEND			= 11,
 	PENDING_FLUSH_STORED_SEND_BEFORE_CLOSE			= 12,
 	PENDING_TIMEOUT_SHUTDOWN_FLUSH				= 13,
+	PENDING_TIMEOUT_CGI					= 14,
 
 	/****** add new things just above ---^ ******/
 };
@@ -1741,6 +1747,10 @@ lws_frame_is_binary(struct lws *wsi);
 
 LWS_VISIBLE LWS_EXTERN int
 lws_is_ssl(struct lws *wsi);
+
+LWS_VISIBLE LWS_EXTERN int
+lws_is_cgi(struct lws *wsi);
+
 #ifdef LWS_SHA1_USE_OPENSSL_NAME
 #define lws_SHA1 SHA1
 #else
@@ -1813,6 +1823,27 @@ lws_get_context(const struct lws *wsi);
 LWS_VISIBLE LWS_EXTERN int LWS_WARN_UNUSED_RESULT
 lws_get_count_threads(struct lws_context *context);
 
+#ifdef LWS_WITH_CGI
+enum lws_enum_stdinouterr {
+	LWS_STDIN = 0,
+	LWS_STDOUT = 1,
+	LWS_STDERR = 2,
+};
+
+struct lws_cgi_args {
+	struct lws **stdwsi; /* get fd with lws_get_socket_fd() */
+	enum lws_enum_stdinouterr ch;
+	unsigned char *data; /* for messages with payload */
+	int len;
+};
+
+LWS_VISIBLE LWS_EXTERN int
+lws_cgi(struct lws *wsi, char * const *exec_array, int timeout_secs);
+
+LWS_VISIBLE LWS_EXTERN int
+lws_cgi_kill(struct lws *wsi);
+#endif
+
 /*
  * Wsi-associated File Operations access helpers
  *
diff --git a/lib/private-libwebsockets.h b/lib/private-libwebsockets.h
index 315c32e1..1de2ca5b 100644
--- a/lib/private-libwebsockets.h
+++ b/lib/private-libwebsockets.h
@@ -22,6 +22,11 @@
 #include "lws_config.h"
 #include "lws_config_private.h"
 
+
+#if defined(LWS_WITH_CGI) && defined(LWS_HAVE_VFORK)
+#define  _GNU_SOURCE
+#endif
+
 #ifdef LWS_HAVE_SYS_TYPES_H
 #include <sys/types.h>
 #endif
@@ -353,6 +358,8 @@ enum lws_connection_states {
 	LWSS_HTTP2_AWAIT_CLIENT_PREFACE,
 	LWSS_HTTP2_ESTABLISHED_PRE_SETTINGS,
 	LWSS_HTTP2_ESTABLISHED,
+
+	LWSS_CGI
 };
 
 enum http_version {
@@ -427,6 +434,7 @@ enum connection_mode {
 
 	/* special internal types */
 	LWSCM_SERVER_LISTENER,
+	LWSCM_CGI, /* stdin, stdout, stderr for another cgi master wsi */
 };
 
 enum {
@@ -530,6 +538,9 @@ struct lws_context_per_thread {
 	struct lws *rx_draining_ext_list;
 	struct lws *tx_draining_ext_list;
 	struct lws *timeout_list;
+#ifdef LWS_WITH_CGI
+	struct lws_cgi *cgi_list;
+#endif
 	void *http_header_data;
 	struct allocated_headers *ah_pool;
 	struct lws *ah_wait_list;
@@ -995,6 +1006,21 @@ struct _lws_websocket_related {
 	unsigned int tx_draining_ext:1;
 };
 
+#ifdef LWS_WITH_CGI
+
+/* wsi who is master of the cgi points to an lws_cgi */
+
+struct lws_cgi {
+	struct lws_cgi *cgi_list;
+	struct lws *stdwsi[3]; /* points to the associated stdin/out/err wsis */
+	struct lws *wsi; /* owner */
+	int pipe_fds[3][2];
+	int pid;
+
+	unsigned int being_closed:1;
+};
+#endif
+
 struct lws {
 
 	/* structs */
@@ -1022,6 +1048,10 @@ struct lws {
 	/* pointers */
 
 	struct lws_context *context;
+#ifdef LWS_WITH_CGI
+	struct lws_cgi *cgi; /* wsi being cgi master have one of these */
+	struct lws *master; /* for stdin/out/err wsi to point to cgi master */
+#endif
 	const struct lws_protocols *protocol;
 	struct lws *timeout_list;
 	struct lws **timeout_list_prev;
@@ -1082,6 +1112,9 @@ struct lws {
 	char pending_timeout; /* enum pending_timeout */
 	char pps; /* enum lws_pending_protocol_send */
 	char tsi; /* thread service index we belong to */
+#ifdef LWS_WITH_CGI
+	char cgi_channel; /* which of stdin/out/err */
+#endif
 };
 
 LWS_EXTERN int log_level;
@@ -1439,6 +1472,9 @@ LWS_EXTERN int
 lws_get_addresses(struct lws_context *context, void *ads, char *name,
 		  int name_len, char *rip, int rip_len);
 
+LWS_EXTERN int
+lws_cgi_kill_terminated(struct lws_context_per_thread *pt);
+
 /*
  * custom allocator
  */
diff --git a/lib/server.c b/lib/server.c
index 92a8eedc..4aef8bd6 100644
--- a/lib/server.c
+++ b/lib/server.c
@@ -609,7 +609,8 @@ upgrade_ws:
 			return 1;
 		}
 #endif
-		lwsl_parser("accepted v%02d connection\n", wsi->ietf_spec_revision);
+		lwsl_parser("accepted v%02d connection\n",
+			    wsi->ietf_spec_revision);
 
 		return 0;
 	} /* while all chars are handled */
@@ -632,7 +633,8 @@ lws_get_idlest_tsi(struct lws_context *context)
 	int n = 0, hit = -1;
 
 	for (; n < context->count_threads; n++) {
-		if ((unsigned int)context->pt[n].fds_count != context->fd_limit_per_thread - 1 &&
+		if ((unsigned int)context->pt[n].fds_count !=
+		    context->fd_limit_per_thread - 1 &&
 		    (unsigned int)context->pt[n].fds_count < lowest) {
 			lowest = context->pt[n].fds_count;
 			hit = n;
diff --git a/lib/service.c b/lib/service.c
index f5dcefc4..f6040825 100644
--- a/lib/service.c
+++ b/lib/service.c
@@ -554,6 +554,9 @@ lws_service_fd_tsi(struct lws_context *context, struct lws_pollfd *pollfd, int t
 			}
 			wsi = wsi1;
 		}
+#ifdef LWS_WITH_CGI
+		lws_cgi_kill_terminated(pt);
+#endif
 #if 0
 		{
 			char s[300], *p = s;
@@ -588,7 +591,6 @@ lws_service_fd_tsi(struct lws_context *context, struct lws_pollfd *pollfd, int t
 	 */
 
 #if LWS_POSIX
-
 	/* handle session socket closed */
 
 	if ((!(pollfd->revents & pollfd->events & LWS_POLLIN)) &&
@@ -607,6 +609,8 @@ lws_service_fd_tsi(struct lws_context *context, struct lws_pollfd *pollfd, int t
 
 #endif
 
+	lwsl_debug("fd=%d, revents=%d\n", pollfd->fd, pollfd->revents);
+
 	/* okay, what we came here to do... */
 
 	switch (wsi->mode) {
@@ -652,7 +656,7 @@ lws_service_fd_tsi(struct lws_context *context, struct lws_pollfd *pollfd, int t
 			wsi->u.ws.tx_draining_ext = 0;
 		}
 
-		if (wsi->u.ws.tx_draining_ext) {
+		if (wsi->u.ws.tx_draining_ext)
 			/* we cannot deal with new RX until the TX ext
 			 * path has been drained.  It's because new
 			 * rx will, eg, crap on the wsi rx buf that
@@ -662,7 +666,6 @@ lws_service_fd_tsi(struct lws_context *context, struct lws_pollfd *pollfd, int t
 			 * to avoid blocking.
 			 */
 			break;
-		}
 
 		if (!(wsi->rxflow_change_to & LWS_RXFLOW_ALLOW))
 			/* We cannot deal with any kind of new RX
@@ -795,7 +798,8 @@ drain:
 		} while (more);
 
 		if (wsi->u.hdr.ah) {
-			lwsl_err("%s: %p: detaching inherited used ah\n", __func__, wsi);
+			lwsl_err("%s: %p: detaching inherited used ah\n",
+				 __func__, wsi);
 			/* show we used all the pending rx up */
 			wsi->u.hdr.ah->rxpos = wsi->u.hdr.ah->rxlen;
 			/* we can run the normal ah detach flow despite
@@ -825,7 +829,39 @@ handle_pending:
 		}
 
 		break;
+#ifdef LWS_WITH_CGI
+	case LWSCM_CGI: /* we exist to handle a cgi's stdin/out/err data...
+			 * do the callback on our master wsi
+			 */
+		{
+			struct lws_cgi_args args;
+
+			if (wsi->cgi_channel >= LWS_STDOUT &&
+			    !(pollfd->revents & pollfd->events & LWS_POLLIN))
+				break;
+			if (wsi->cgi_channel == LWS_STDIN &&
+			    !(pollfd->revents & pollfd->events & LWS_POLLOUT))
+				break;
+
+			if (wsi->cgi_channel == LWS_STDIN)
+				if (lws_change_pollfd(wsi, LWS_POLLOUT, 0)) {
+					lwsl_info("failed at set pollfd\n");
+					return 1;
+				}
+
+			args.ch = wsi->cgi_channel;
+			args.stdwsi = &wsi->master->cgi->stdwsi[0];
 
+			if (user_callback_handle_rxflow(
+					wsi->master->protocol->callback,
+					wsi->master, LWS_CALLBACK_CGI,
+					wsi->master->user_space,
+					(void *)&args, 0))
+				return 1;
+
+			break;
+		}
+#endif
 	default:
 #ifdef LWS_NO_CLIENT
 		break;
diff --git a/lws_config.h.in b/lws_config.h.in
index 6ce964b6..c108eb3a 100644
--- a/lws_config.h.in
+++ b/lws_config.h.in
@@ -74,6 +74,9 @@
 /* SSL server using ECDH certificate */
 #cmakedefine LWS_SSL_SERVER_WITH_ECDH_CERT
 
+/* CGI apis */
+#cmakedefine LWS_WITH_CGI
+
 /* Maximum supported service threads */
 #define LWS_MAX_SMP ${LWS_MAX_SMP}
 
diff --git a/test-server/lws-cgi-test.sh b/test-server/lws-cgi-test.sh
new file mode 100755
index 00000000..90c804e5
--- /dev/null
+++ b/test-server/lws-cgi-test.sh
@@ -0,0 +1,16 @@
+#!/bin/sh
+
+echo "lwstest script stdout"
+>&2 echo "lwstest script stderr"
+
+echo "REQUEST_METHOD=$REQUEST_METHOD"
+
+if [ "$REQUEST_METHOD" = "POST" ] ; then
+	read line
+	echo "read=\"$line\""
+fi
+
+echo "done"
+
+exit 0
+
diff --git a/test-server/test-server-http.c b/test-server/test-server-http.c
index 9cabce58..c1433436 100644
--- a/test-server/test-server-http.c
+++ b/test-server/test-server-http.c
@@ -129,7 +129,6 @@ int callback_http(struct lws *wsi, enum lws_callback_reasons reason, void *user,
 	char buf[256];
 	char b64[64];
 	int n, m;
-
 #ifdef EXTERNAL_POLL
 	struct lws_pollargs *pa = (struct lws_pollargs *)in;
 #endif
@@ -159,6 +158,43 @@ int callback_http(struct lws *wsi, enum lws_callback_reasons reason, void *user,
 			goto try_to_reuse;
 		}
 
+#ifdef LWS_WITH_CGI
+		if (!strcmp(in, "/cgitest")) {
+			static char *cmd[] = {
+				"/bin/sh",
+				"-c",
+				INSTALL_DATADIR"/libwebsockets-test-server/lws-cgi-test.sh",
+				NULL
+			};
+
+			lwsl_notice("%s: cgitest\n", __func__);
+			n = lws_cgi(wsi, cmd, 5);
+			if (n) {
+				lwsl_err("%s: cgi failed\n");
+				return -1;
+			}
+			p = buffer + LWS_PRE;
+			end = p + sizeof(buffer) - LWS_PRE;
+
+			if (lws_add_http_header_status(wsi, 200, &p, end))
+				return 1;
+			if (lws_add_http_header_by_token(wsi,
+					WSI_TOKEN_HTTP_CONTENT_TYPE,
+					(unsigned char *)"text/plain",
+					10, &p, end))
+				return 1;
+			if (lws_add_http_header_by_token(wsi, WSI_TOKEN_CONNECTION,
+					(unsigned char *)"close", 5, &p, end))
+				return 1;
+			if (lws_finalize_http_header(wsi, &p, end))
+				return 1;
+			n = lws_write(wsi, buffer + LWS_PRE,
+				      p - (buffer + LWS_PRE),
+				      LWS_WRITE_HTTP_HEADERS);
+			break;
+		}
+#endif
+
 		/* if a legal POST URL, let it continue and accept data */
 		if (lws_hdr_total_length(wsi, WSI_TOKEN_POST_URI))
 			return 0;
@@ -225,7 +261,8 @@ int callback_http(struct lws *wsi, enum lws_callback_reasons reason, void *user,
 			*p = '\0';
 			lwsl_info("%s\n", buffer + LWS_PRE);
 
-			n = lws_write(wsi, buffer + LWS_PRE, p - (buffer + LWS_PRE),
+			n = lws_write(wsi, buffer + LWS_PRE,
+				      p - (buffer + LWS_PRE),
 				      LWS_WRITE_HTTP_HEADERS);
 			if (n < 0) {
 				lws_plat_file_close(wsi, pss->fd);
@@ -322,7 +359,25 @@ int callback_http(struct lws *wsi, enum lws_callback_reasons reason, void *user,
 
 		if (pss->fd == LWS_INVALID_FILE)
 			goto try_to_reuse;
-
+#ifdef LWS_WITH_CGI
+		if (pss->reason_bf) {
+			lwsl_debug("%s: stdout\n", __func__);
+			n = read(lws_get_socket_fd(pss->args.stdwsi[LWS_STDOUT]),
+					buf + LWS_PRE, sizeof(buf) - LWS_PRE);
+			//lwsl_notice("read %d (errno %d)\n", n, errno);
+			if (n < 0 && errno != EAGAIN)
+				return -1;
+			if (n > 0) {
+				m = lws_write(wsi, (unsigned char *)buf + LWS_PRE, n,
+					      LWS_WRITE_HTTP);
+				//lwsl_notice("write %d\n", m);
+				if (m < 0)
+					goto bail;
+				pss->reason_bf = 0;
+			}
+			break;
+		}
+#endif
 		/*
 		 * we can send more of whatever it is we were sending
 		 */
@@ -393,10 +448,59 @@ bail:
 	 * connection continue.
 	 */
 	case LWS_CALLBACK_FILTER_NETWORK_CONNECTION:
-
 		/* if we returned non-zero from here, we kill the connection */
 		break;
 
+#ifdef LWS_WITH_CGI
+	/* CGI IO events (POLLIN/OUT) appear here our demo user code policy is
+	 *
+	 *  - POST data goes on subprocess stdin
+	 *  - subprocess stdout goes on http via writeable callback
+	 *  - subprocess stderr goes to the logs
+	 */
+	case LWS_CALLBACK_CGI:
+		pss->args = *((struct lws_cgi_args *)in);
+		//lwsl_notice("LWS_CALLBACK_CGI: ch %d\n", pss->args.ch);
+		switch (pss->args.ch) { /* which of stdin/out/err ? */
+		case LWS_STDIN:
+			/* TBD stdin rx flow control */
+			break;
+		case LWS_STDOUT:
+			pss->reason_bf |= 1 << pss->args.ch;
+			/* when writing to MASTER would not block */
+			lws_callback_on_writable(wsi);
+			break;
+		case LWS_STDERR:
+			n = read(lws_get_socket_fd(pss->args.stdwsi[LWS_STDERR]),
+					buf, 127);
+			//lwsl_notice("stderr reads %d\n", n);
+			if (n > 0) {
+				if (buf[n - 1] != '\n')
+					buf[n++] = '\n';
+				buf[n] = '\0';
+				lwsl_notice("CGI-stderr: %s\n", buf);
+			}
+			break;
+		}
+		break;
+
+	case LWS_CALLBACK_CGI_TERMINATED:
+		lwsl_notice("LWS_CALLBACK_CGI_TERMINATED\n");
+		/* because we sent on openended http, close the connection */
+		return -1;
+
+	case LWS_CALLBACK_CGI_STDIN_DATA:  /* POST body for stdin */
+		lwsl_notice("LWS_CALLBACK_CGI_STDIN_DATA\n");
+		pss->args = *((struct lws_cgi_args *)in);
+		n = write(lws_get_socket_fd(pss->args.stdwsi[LWS_STDIN]),
+			  pss->args.data, pss->args.len);
+		lwsl_notice("LWS_CALLBACK_CGI_STDIN_DATA: write says %d", n);
+		if (n < pss->args.len)
+			lwsl_notice("LWS_CALLBACK_CGI_STDIN_DATA: sent %d only %d went",
+					n, pss->args.len);
+		return n;
+#endif
+
 	/*
 	 * callbacks for managing the external poll() array appear in
 	 * protocol 0 callback
diff --git a/test-server/test-server.h b/test-server/test-server.h
index cb69d500..7217621e 100644
--- a/test-server/test-server.h
+++ b/test-server/test-server.h
@@ -67,6 +67,10 @@ extern void test_server_unlock(int care);
 
 struct per_session_data__http {
 	lws_filefd_type fd;
+#ifdef LWS_WITH_CGI
+	struct lws_cgi_args args;
+	int reason_bf;
+#endif
 };
 
 /*
-- 
GitLab